lot of changes

This commit is contained in:
Andre Henriques 2024-11-21 12:19:43 +00:00
parent dd047c0bcf
commit 26301d1a13
32 changed files with 1740 additions and 311 deletions

View File

@ -2,6 +2,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://kronos.home:5432/applications spring.datasource.url=jdbc:postgresql://kronos.home:5432/applications
spring.datasource.username=applications spring.datasource.username=applications
spring.datasource.password=applications spring.datasource.password=applications
spring-boot.run.jvmArguments=-Duser.timezone=UTC
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Disable the trace on the error responses # Disable the trace on the error responses

View File

@ -33,8 +33,10 @@ data class Application(
var extra_data: String, var extra_data: String,
var payrange: String, var payrange: String,
var status: Int, var status: Int,
var status_id: String?,
var company: String, var company: String,
var recruiter: String, var recruiter: String,
var agency: Boolean,
var message: String, var message: String,
var linked_application: String, var linked_application: String,
var status_history: String, var status_history: String,
@ -56,8 +58,10 @@ data class Application(
rs.getString("extra_data"), rs.getString("extra_data"),
rs.getString("payrange"), rs.getString("payrange"),
rs.getInt("status"), rs.getInt("status"),
rs.getString("status_id"),
rs.getString("company"), rs.getString("company"),
rs.getString("recruiter"), rs.getString("recruiter"),
rs.getBoolean("agency"),
rs.getString("message"), rs.getString("message"),
rs.getString("linked_application"), rs.getString("linked_application"),
rs.getString("status_history"), rs.getString("status_history"),
@ -85,6 +89,7 @@ data class CVData(
val company: String, val company: String,
val recruiter: String, val recruiter: String,
val message: String, val message: String,
val agency: Boolean,
val flairs: List<SimpleFlair> val flairs: List<SimpleFlair>
) )
@ -113,7 +118,7 @@ class ApplicationsController(
val flairs = application.flairs.map { it.toFlairSimple() } val flairs = application.flairs.map { it.toFlairSimple() }
return CVData(application.company, application.recruiter, application.message, flairs) return CVData(application.company, application.recruiter, application.message, application.agency, flairs)
} }
/** Create a new application from the link */ /** Create a new application from the link */
@ -135,8 +140,10 @@ class ApplicationsController(
"", "",
"", "",
0, 0,
null,
"", "",
"", "",
false,
"", "",
"", "",
"", "",
@ -263,8 +270,10 @@ class ApplicationsController(
"", "",
"", "",
0, 0,
null,
"", "",
"", "",
false,
"", "",
"", "",
"", "",
@ -535,8 +544,9 @@ class ApplicationService(
} }
// Create time is auto created by the database // Create time is auto created by the database
// The default status is null
db.update( db.update(
"insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", "insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time, agency) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
application.id, application.id,
application.url, application.url,
application.original_url, application.original_url,
@ -552,6 +562,7 @@ class ApplicationService(
application.linked_application, application.linked_application,
application.status_history, application.status_history,
application.application_time, application.application_time,
application.agency,
) )
eventService.create(application.id, EventType.Creation) eventService.create(application.id, EventType.Creation)
@ -595,7 +606,16 @@ class ApplicationService(
return iter.toList() return iter.toList()
} }
public fun findAllByUserStatusId(statusId: String, user: UserDb): List<Application> {
return db.query(
"select * from applications where status_id=? and user_id=? order by title asc;",
arrayOf(statusId, user.id),
Application
)
}
// Update the stauts on the application object before giving it to this function // Update the stauts on the application object before giving it to this function
// TODO how status history works
public fun updateStatus(application: Application): Application { public fun updateStatus(application: Application): Application {
val status_string = "${application.status}" val status_string = "${application.status}"
@ -627,7 +647,7 @@ class ApplicationService(
public fun update(application: Application): Application { public fun update(application: Application): Application {
// I don't want ot update create_time // I don't want ot update create_time
db.update( db.update(
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, linked_application=? where id=?", "update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, linked_application=?, agency=? where id=?",
application.url, application.url,
application.original_url, application.original_url,
application.unique_url, application.unique_url,
@ -639,6 +659,7 @@ class ApplicationService(
application.recruiter, application.recruiter,
application.message, application.message,
application.linked_application, application.linked_application,
application.agency,
application.id, application.id,
) )
return application return application

View File

@ -0,0 +1,301 @@
package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.util.UUID
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
data class UserStatusNode(
var id: String,
var user_id: String,
var name: String,
var icon: String,
var x: Int,
var y: Int,
var width: Int,
var height: Int,
var permission: Int
) {
companion object : RowMapper<UserStatusNode> {
override public fun mapRow(rs: ResultSet, rowNum: Int): UserStatusNode {
return UserStatusNode(
rs.getString("id"),
rs.getString("user_id"),
rs.getString("name"),
rs.getString("icon"),
rs.getInt("x"),
rs.getInt("y"),
rs.getInt("width"),
rs.getInt("height"),
rs.getInt("permission"),
)
}
}
}
data class UserStatusLink(
var id: String,
var user_id: String,
var source_node: String?,
var target_node: String?,
var bi: Boolean,
var source_x: Int,
var source_y: Int,
var target_x: Int,
var target_y: Int,
) {
companion object : RowMapper<UserStatusLink> {
override public fun mapRow(rs: ResultSet, rowNum: Int): UserStatusLink {
return UserStatusLink(
rs.getString("id"),
rs.getString("user_id"),
rs.getString("source_node"),
rs.getString("target_node"),
rs.getBoolean("bi"),
rs.getInt("source_x"),
rs.getInt("source_y"),
rs.getInt("target_x"),
rs.getInt("target_y"),
)
}
}
}
@RestController
@ControllerAdvice
@RequestMapping("/api/user/status")
class UserApplicationStatusController(
val sessionService: SessionService,
val userStatusNodeService: UserStatusNodeService,
val userStatusLinkService: UserStatusLinkService,
val applicationService: ApplicationService,
) {
//
// Nodes
//
@GetMapping(path = ["/node"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun listAll(@RequestHeader("token") token: String): List<UserStatusNode> {
val user = sessionService.verifyTokenThrow(token)
return userStatusNodeService.findAllByUserId(user.id)
}
@PostMapping(path = ["/node"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun create(
@RequestBody node: UserStatusNode,
@RequestHeader("token") token: String
): UserStatusNode {
val user = sessionService.verifyTokenThrow(token)
node.user_id = user.id
return userStatusNodeService.create(node)
}
@PutMapping(path = ["/node"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun put(
@RequestBody node: UserStatusNode,
@RequestHeader("token") token: String
): UserStatusNode? {
val user = sessionService.verifyTokenThrow(token)
if (userStatusNodeService.findById(node.id, user) == null) {
throw NotFound()
}
return userStatusNodeService.update(node)
}
@DeleteMapping(path = ["/node/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun delete(@PathVariable id: String, @RequestHeader("token") token: String): Boolean {
val user = sessionService.verifyTokenThrow(token)
val node = userStatusNodeService.findById(id, user)
if (node == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find a node")
}
val applications = applicationService.findAllByUserStatusId(id, user)
if (applications.size > 0) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Applications exist with this user status!")
}
userStatusLinkService.deleteByNode(user, node)
userStatusNodeService.delete(node, user)
return true
}
//
// Link
//
@GetMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun listAllLinks(@RequestHeader("token") token: String): List<UserStatusLink> {
val user = sessionService.verifyTokenThrow(token)
return userStatusLinkService.findAllByUserId(user)
}
@PutMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun createLink(
@RequestBody link: UserStatusLink,
@RequestHeader("token") token: String
): UserStatusLink {
val user = sessionService.verifyTokenThrow(token)
if (link.source_node == link.target_node) {
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Request Node and Target Not can not be the same"
)
}
if (link.source_node != null &&
userStatusNodeService.findById(link.source_node!!, user) == null
) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Request source not found")
}
if (link.target_node != null &&
userStatusNodeService.findById(link.target_node!!, user) == null
) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Request target not found")
}
link.user_id = user.id
return userStatusLinkService.create(link)
}
}
@Service
public class UserStatusNodeService(val db: JdbcTemplate) {
public fun findById(id: String, user: UserDb): UserStatusNode? {
var nodes =
db.query(
"select * from user_status_node where id=? and user_id=?",
arrayOf(id, user.id),
UserStatusNode
)
if (nodes.size == 0) {
return null
}
return nodes[0]
}
public fun findAllByUserId(user_id: String): List<UserStatusNode> {
return db.query(
"select * from user_status_node where user_id=?",
arrayOf(user_id),
UserStatusNode
)
.toList()
}
public fun update(node: UserStatusNode): UserStatusNode {
db.update(
"update user_status_node set name=?, icon=?, x=?, y=?, width=?, height=?, permission=? where id=?;",
node.name,
node.icon,
node.x,
node.y,
node.width,
node.height,
node.permission,
node.id,
)
return node
}
public fun create(node: UserStatusNode): UserStatusNode {
val id = UUID.randomUUID().toString()
node.id = id
db.update(
"insert into user_status_node (id, user_id, name, icon, x, y, width, height, permission) values (?, ?, ?, ?, ?, ?, ?, ?, ?);",
node.id,
node.user_id,
node.name,
node.icon,
node.x,
node.y,
node.width,
node.height,
node.permission
)
return node
}
public fun delete(node: UserStatusNode, user: UserDb) {
db.update("delete from user_status_node where id=? and user_id=?;", node.id, user.id)
}
}
@Service
public class UserStatusLinkService(val db: JdbcTemplate) {
public fun findById(id: String, user: UserDb): UserStatusLink? {
val links =
db.query(
"select * from user_status_link where user_id=? and id=?;",
arrayOf(id, user.id),
UserStatusLink
)
if (links.size == 0) {
return null
}
return links[0]
}
public fun findAllByUserId(user: UserDb): List<UserStatusLink> {
return db.query(
"select * from user_status_link where user_id=?",
arrayOf(user.id),
UserStatusLink
)
.toList()
}
public fun deleteByNode(user: UserDb, node: UserStatusNode) {
db.update("delete from user_status_link where (target_node=? or source_node=?) and user_id=?;", node.id, node.id, user.id)
}
public fun create(link: UserStatusLink): UserStatusLink {
val id = UUID.randomUUID().toString()
link.id = id
db.update(
"insert into user_status_link (id, user_id, source_node, target_node, bi, source_x, source_y, target_x, target_y) values (?, ?, ?, ?, ?, ?, ?, ?, ?);",
link.id,
link.user_id,
link.source_node,
link.target_node,
link.bi,
link.source_x,
link.source_y,
link.target_x,
link.target_y,
)
return link
}
}

View File

@ -23,16 +23,20 @@ create table if not exists applications (
status_history text default '', status_history text default '',
user_id text, user_id text,
extra_data text, extra_data text,
-- this status will be deprecated in favor of the node style status
status integer, status integer,
status_id text default null,
linked_application text default '', linked_application text default '',
application_time text default '', application_time text default '',
agency boolean default false,
create_time timestamp default now() create_time timestamp default now()
); );
-- Views are deprecated will be removed in the future
create table if not exists views ( create table if not exists views (
id text primary key, id text primary key,
application_id text not null, application_id text not null,
time timestamp default current_timestamp time timestamp default now()
); );
create table if not exists flair ( create table if not exists flair (
@ -65,5 +69,41 @@ create table if not exists events (
-- This only matters when event_type == 1 -- This only matters when event_type == 1
new_status integer, new_status integer,
time timestamp default current_timestamp time timestamp default now()
);
--
-- User Controlled Status
--
create table if not exists user_status_node (
id text primary key,
user_id text not null,
name text not null,
icon text not null,
x integer default 0,
y integer default 0,
width integer default 0,
height integer default 0,
permission integer default 0
);
create table if not exists user_status_link (
id text primary key,
-- You technically can get this by loking a the source and target nodes but that seams more complicated
user_id text not null,
-- This can be null because null means creation
source_node text,
-- This can be null because null means creation
target_node text,
source_x integer default 0,
source_y integer default 0,
target_x integer default 0,
target_y integer default 0,
-- If this link is bidiretoral
bi boolean
); );

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"bootstrap-icons": "^1.11.3"
}
}

22
pnpm-lock.yaml Normal file
View File

@ -0,0 +1,22 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
bootstrap-icons:
specifier: ^1.11.3
version: 1.11.3
packages:
bootstrap-icons@1.11.3:
resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==}
snapshots:
bootstrap-icons@1.11.3: {}

View File

@ -18,6 +18,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/eslint": "^8.56.7", "@types/eslint": "^8.56.7",
"@types/node": "^22.8.7",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3": "^7.9.0", "d3": "^7.9.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
@ -35,5 +36,8 @@
"typescript-eslint": "^8.0.0-alpha.20", "typescript-eslint": "^8.0.0-alpha.20",
"vite": "^5.0.3" "vite": "^5.0.3"
}, },
"type": "module" "type": "module",
"dependencies": {
"bootstrap-icons": "1.11.3"
}
} }

View File

@ -7,25 +7,32 @@ settings:
importers: importers:
.: .:
dependencies:
bootstrap-icons:
specifier: 1.11.3
version: 1.11.3
devDependencies: devDependencies:
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.2.2(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3)) version: 3.2.2(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3)) version: 3.0.5(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3) version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3) version: 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
'@types/d3': '@types/d3':
specifier: ^7.4.3 specifier: ^7.4.3
version: 7.4.3 version: 7.4.3
'@types/eslint': '@types/eslint':
specifier: ^8.56.7 specifier: ^8.56.7
version: 8.56.10 version: 8.56.10
'@types/node':
specifier: ^22.8.7
version: 22.8.7
autoprefixer: autoprefixer:
specifier: ^10.4.19 specifier: ^10.4.19
version: 10.4.19(postcss@8.4.39) version: 10.4.19(postcss@8.4.39)
@ -73,7 +80,7 @@ importers:
version: 8.0.0-alpha.39(eslint@9.6.0)(typescript@5.5.3) version: 8.0.0-alpha.39(eslint@9.6.0)(typescript@5.5.3)
vite: vite:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.3.3 version: 5.3.3(@types/node@22.8.7)
packages: packages:
@ -520,6 +527,9 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.8.7':
resolution: {integrity: sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==}
'@types/pug@2.0.10': '@types/pug@2.0.10':
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
@ -651,6 +661,9 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
bootstrap-icons@1.11.3:
resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==}
brace-expansion@1.1.11: brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@ -1674,6 +1687,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
update-browserslist-db@1.1.0: update-browserslist-db@1.1.0:
resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
hasBin: true hasBin: true
@ -1964,18 +1980,18 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.18.0': '@rollup/rollup-win32-x64-msvc@4.18.0':
optional: true optional: true
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3))': '@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))':
dependencies: dependencies:
'@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3) '@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
import-meta-resolve: 4.1.0 import-meta-resolve: 4.1.0
'@sveltejs/adapter-static@3.0.5(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3))': '@sveltejs/adapter-static@3.0.5(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))':
dependencies: dependencies:
'@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3) '@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
'@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3)': '@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3) '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
cookie: 0.6.0 cookie: 0.6.0
devalue: 5.0.0 devalue: 5.0.0
@ -1989,28 +2005,28 @@ snapshots:
sirv: 2.0.4 sirv: 2.0.4
svelte: 5.0.0-next.174 svelte: 5.0.0-next.174
tiny-glob: 0.2.9 tiny-glob: 0.2.9
vite: 5.3.3 vite: 5.3.3(@types/node@22.8.7)
'@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3)': '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3) '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
debug: 4.3.5 debug: 4.3.5
svelte: 5.0.0-next.174 svelte: 5.0.0-next.174
vite: 5.3.3 vite: 5.3.3(@types/node@22.8.7)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3)': '@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3))(svelte@5.0.0-next.174)(vite@5.3.3) '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7)))(svelte@5.0.0-next.174)(vite@5.3.3(@types/node@22.8.7))
debug: 4.3.5 debug: 4.3.5
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.30.10 magic-string: 0.30.10
svelte: 5.0.0-next.174 svelte: 5.0.0-next.174
svelte-hmr: 0.16.0(svelte@5.0.0-next.174) svelte-hmr: 0.16.0(svelte@5.0.0-next.174)
vite: 5.3.3 vite: 5.3.3(@types/node@22.8.7)
vitefu: 0.2.5(vite@5.3.3) vitefu: 0.2.5(vite@5.3.3(@types/node@22.8.7))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2144,6 +2160,10 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@22.8.7':
dependencies:
undici-types: 6.19.8
'@types/pug@2.0.10': {} '@types/pug@2.0.10': {}
'@typescript-eslint/eslint-plugin@8.0.0-alpha.39(@typescript-eslint/parser@8.0.0-alpha.39(eslint@9.6.0)(typescript@5.5.3))(eslint@9.6.0)(typescript@5.5.3)': '@typescript-eslint/eslint-plugin@8.0.0-alpha.39(@typescript-eslint/parser@8.0.0-alpha.39(eslint@9.6.0)(typescript@5.5.3))(eslint@9.6.0)(typescript@5.5.3)':
@ -2289,6 +2309,8 @@ snapshots:
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
bootstrap-icons@1.11.3: {}
brace-expansion@1.1.11: brace-expansion@1.1.11:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@ -3330,6 +3352,8 @@ snapshots:
typescript@5.5.3: {} typescript@5.5.3: {}
undici-types@6.19.8: {}
update-browserslist-db@1.1.0(browserslist@4.23.1): update-browserslist-db@1.1.0(browserslist@4.23.1):
dependencies: dependencies:
browserslist: 4.23.1 browserslist: 4.23.1
@ -3342,17 +3366,18 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite@5.3.3: vite@5.3.3(@types/node@22.8.7):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.4.39 postcss: 8.4.39
rollup: 4.18.0 rollup: 4.18.0
optionalDependencies: optionalDependencies:
'@types/node': 22.8.7
fsevents: 2.3.3 fsevents: 2.3.3
vitefu@0.2.5(vite@5.3.3): vitefu@0.2.5(vite@5.3.3(@types/node@22.8.7)):
optionalDependencies: optionalDependencies:
vite: 5.3.3 vite: 5.3.3(@types/node@22.8.7)
which@2.0.2: which@2.0.2:
dependencies: dependencies:

View File

@ -5,11 +5,6 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body class="grad-back" data-sveltekit-preload-data="hover"> <body class="grad-back" data-sveltekit-preload-data="hover">

View File

@ -11,9 +11,11 @@ export const ApplicationStatus = Object.freeze({
Applyed: 4, Applyed: 4,
Expired: 5, Expired: 5,
TasksToDo: 6, TasksToDo: 6,
TasksToDo2: 10,
LinkedApplication: 7, LinkedApplication: 7,
InterviewStep1: 8, InterviewStep1: 8,
InterviewStep2: 9 InterviewStep2: 9,
FinalInterview: 11
}); });
export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({ export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
@ -24,9 +26,11 @@ export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus
4: 'send', 4: 'send',
5: 'hourglass-bottom', 5: 'hourglass-bottom',
6: 'list-check', 6: 'list-check',
10: 'list-check',
7: 'link-45deg', 7: 'link-45deg',
8: 'person', 8: 'person',
9: 'people' 9: 'people',
11: 'badge-vo-fill'
}); });
export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({ export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
@ -37,9 +41,11 @@ export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, s
4: 'Applyed', 4: 'Applyed',
5: 'Expired', 5: 'Expired',
6: 'Tasks To Do', 6: 'Tasks To Do',
10: 'Tasks To Do 2',
7: 'Linked Application', 7: 'Linked Application',
8: 'Interview 1', 8: 'Interview 1',
9: 'Interview 2' 9: 'Interview 2',
11: 'Final Interview'
}); });
export type View = { export type View = {
@ -73,6 +79,7 @@ export type Application = {
payrange: string; payrange: string;
status: AsEnum<typeof ApplicationStatus>; status: AsEnum<typeof ApplicationStatus>;
recruiter: string; recruiter: string;
agency: boolean;
company: string; company: string;
message: string; message: string;
linked_application: string; linked_application: string;

View File

@ -1,5 +1,6 @@
<script> <script>
import '../app.css'; import '../app.css';
import 'bootstrap-icons/font/bootstrap-icons.min.css'
</script> </script>
<slot /> <slot />

View File

@ -16,9 +16,18 @@
<ApplicationsList /> <ApplicationsList />
<WorkArea /> <WorkArea />
</div> </div>
<PApplicationList status={ApplicationStatus.FinalInterview}>
Interview Final
</PApplicationList >
<PApplicationList status={ApplicationStatus.InterviewStep2}>
Interview II
</PApplicationList >
<PApplicationList status={ApplicationStatus.InterviewStep1}> <PApplicationList status={ApplicationStatus.InterviewStep1}>
Interview I Interview I
</PApplicationList > </PApplicationList >
<PApplicationList status={ApplicationStatus.TasksToDo2}>
Tasks To do 2
</PApplicationList >
<PApplicationList status={ApplicationStatus.TasksToDo}> <PApplicationList status={ApplicationStatus.TasksToDo}>
Tasks To do Tasks To do
</PApplicationList > </PApplicationList >

View File

@ -7,18 +7,9 @@
onMount(() => { onMount(() => {
applicationStore.loadApplications(); applicationStore.loadApplications();
}); });
</script>
<div class="w-2/12 card p-3 flex flex-col flex-shrink min-h-0"> let internal = $derived(
<h1>To Apply</h1> applicationStore.applications.filter((i) => {
<div class="flex">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{applicationStore.applications.length}
</div>
</div>
<div class="overflow-auto flex-grow p-2">
{#each applicationStore.applications.filter((i) => {
if (!filter) { if (!filter) {
return true; return true;
} }
@ -31,7 +22,20 @@
} }
return x.match(f); return x.match(f);
}) as item} })
);
</script>
<div class="w-2/12 card p-3 flex flex-col flex-shrink min-h-0">
<h1>To Apply</h1>
<div class="flex pb-2">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{internal.length}
</div>
</div>
<div class="overflow-auto flex-grow p-2">
{#each internal as item}
<div <div
class="card p-2 my-2 bg-slate-100 max-w-full" class="card p-2 my-2 bg-slate-100 max-w-full"
draggable="true" draggable="true"
@ -43,7 +47,10 @@
}} }}
role="none" role="none"
> >
<div class="max-w-full" class:animate-pulse={applicationStore.dragging?.id === item.id}> <div
class="max-w-full"
class:animate-pulse={applicationStore.dragging?.id === item.id}
>
<h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden"> <h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden">
<div class="flex-grow max-w-[90%]"> <div class="flex-grow max-w-[90%]">
<div class="whitespace-nowrap overflow-hidden"> <div class="whitespace-nowrap overflow-hidden">

View File

@ -10,11 +10,11 @@
let filter = $state(''); let filter = $state('');
</script> </script>
<div class="card p-3 rounded-lg flex flex-col"> <div class="card p-3 rounded-lg">
<h1 class="flex gap-2"> <h1 class="flex gap-2">
Applied <input bind:value={filter} placeholder="search" class="flex-grow text-blue-500" /> Applied <input bind:value={filter} placeholder="search" class="flex-grow text-blue-500" />
</h1> </h1>
<div class="overflow-auto flex-grow"> <div class="flex flex-wrap gap-4 justify-between">
{#each applicationStore.applyed.filter((i) => { {#each applicationStore.applyed.filter((i) => {
if (!filter) { if (!filter) {
return true; return true;
@ -30,10 +30,10 @@
return x.match(f); return x.match(f);
}) as item} }) as item}
<button <button
class="card p-2 my-2 bg-slate-100 w-full text-left" class="card p-2 my-2 bg-slate-100 text-left"
onclick={async () => { onclick={async () => {
item.views = await get(`view/${item.id}`); item.views = await get(`view/${item.id}`);
item.events = await get(`events/${item.id}`); item.events = await get(`events/${item.id}`);
applicationStore.loadItem = item; applicationStore.loadItem = item;
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
@ -50,14 +50,10 @@
</div> </div>
{/if} {/if}
</h2> </h2>
<a
href={item.url}
class="text-violet-600 overflow-hidden whitespace-nowrap block"
>
{item.url}
</a>
</div> </div>
</button> </button>
{/each} {/each}
</div> </div>
</div> </div>
<div class="min-h-[40px]">
</div>

View File

@ -3,7 +3,7 @@
import { userStore } from '$lib/UserStore.svelte'; import { userStore } from '$lib/UserStore.svelte';
</script> </script>
<div class="p-7"> <div class="p-7 pb-1">
<div class="card p-2 rounded-xl flex"> <div class="card p-2 rounded-xl flex">
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}> Home </button> <button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}> Home </button>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/submit')}> <button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/submit')}>
@ -15,6 +15,9 @@
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/graphs')}> <button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/graphs')}>
Graphs Graphs
</button> </button>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flow')}>
Flow
</button>
<div class="flex-grow"></div> <div class="flex-grow"></div>
<div class="text-secudanry px-2"> <div class="text-secudanry px-2">
{userStore.user.username} {userStore.user.username}

View File

@ -44,12 +44,6 @@
</div> </div>
{/if} {/if}
</h2> </h2>
<a
href={item.url}
class="text-violet-600 overflow-hidden whitespace-nowrap block"
>
{item.url}
</a>
</div> </div>
</button> </button>
{/each} {/each}

View File

@ -2,6 +2,7 @@
import { userStore } from '$lib/UserStore.svelte'; import { userStore } from '$lib/UserStore.svelte';
import { get } from '$lib/utils'; import { get } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ApplicationsList from '../ApplicationsList.svelte';
let id: string | undefined | null = $state(undefined); let id: string | undefined | null = $state(undefined);
@ -23,6 +24,7 @@
recruiter: string; recruiter: string;
message: string; message: string;
company: string; company: string;
agency: boolean,
flairs: SimpleFlair[]; flairs: SimpleFlair[];
}; };
@ -127,15 +129,17 @@
</div> </div>
</div> </div>
{#if application} {#if application}
<h2 class="text-white p-3 text-4xl"> {#if !application.agency}
👋 Hello <h2 class="text-white p-3 text-4xl">
{#if application.recruiter} 👋 Hello
<span class="font-bold">{application.recruiter}</span> @ {#if application.recruiter}
<span class="font-bold">{application.company}</span> <span class="font-bold">{application.recruiter}</span> @
{:else if application.company} <span class="font-bold">{application.company}</span>
recruiter @ <span class="font-bold">{application.company}</span> {:else if application.company}
{/if} recruiter @ <span class="font-bold">{application.company}</span>
</h2> {/if}
</h2>
{/if}
{#if application.message} {#if application.message}
<div class="p-3 bg-white w-[190mm] rounded-lg"> <div class="p-3 bg-white w-[190mm] rounded-lg">

View File

@ -0,0 +1,513 @@
<script lang="ts">
import { onMount } from 'svelte';
import NavBar from '../NavBar.svelte';
import Link from './Link.svelte';
import NodeRect from './NodeRect.svelte';
import Rect from './Rect.svelte';
import type { Node, ActionType, LinkNode, FullLink, FullLinkApi } from './types';
import { extractLinkNodePosX, extractLinkNodePosY } from './types';
import { deleteR, get, post, put } from '$lib/utils';
import icon_list from './icons-list.json';
import IconPicker from './IconPicker.svelte';
// TODOS: well a lot of stuff but
// - API
// - Automaticaly move to select mode if one of the targets fals inside the bounds
//
// constatns
//
const grid_size = 35;
//
// binds
//
let activeNode: Node | undefined = $state();
let linkMode = $state(false);
//
// Data
//
let nodes: Node[] = $state([]);
let links: FullLink[] = $state([]);
// Load Data
onMount(async () => {
try {
const nodesRequest = await get('user/status/node');
nodes = [
{
id: null as any,
user_id: null as any,
x: -4,
y: 10,
width: 8,
height: 4,
name: 'Created',
icon: 'plus',
permission: 1
} as Node,
...nodesRequest
];
const nodesAsMap: Record<string, Node> = {};
for (const node of nodes) {
nodesAsMap[node.id] = node;
}
const linkRequests: FullLinkApi[] = await get('user/status/link');
links = linkRequests.map((a) => ({
id: a.id,
bi: a.bi,
sourceNode: {
node: nodesAsMap[a.source_node as any],
x: a.source_x,
y: a.source_y
},
targetNode: {
node: nodesAsMap[a.target_node as any],
x: a.target_x,
y: a.target_y
}
}));
console.log("test", linkRequests)
} catch (e) {
console.log('TODO inform user', e);
}
});
//
// Display stuff
//
// The canvas is not really a canvas but a div will kinda act like a canvas
let canvas: HTMLDivElement | undefined = $state();
//
// Position + movement + interation
//
//
let y = $state(0);
let x = $state(0);
let initialGridOffsetX = $state(0);
let initialGridOffsetY = $state(0);
$effect(() => {
let box = canvas?.getBoundingClientRect();
if (!box) return;
let w = box.width;
let h = box.height;
initialGridOffsetX = Math.round(w / 2 - Math.round(w / 2 / grid_size) * grid_size);
initialGridOffsetY = Math.round(h / 2 - Math.floor(h / 2 / grid_size) * grid_size);
});
// Holdes the position for pan and select
let startMX = $state(0);
let startMY = $state(0);
let startX = $state(0);
let startY = $state(0);
let mouseAction: ActionType = $state(undefined);
let tempMouseAction: ActionType = $state(undefined);
let curPosX = $state(0);
let curPosY = $state(0);
//
// Mouse interation
//
// Right click and drag to move around
function onmousedown(e: MouseEvent) {
// clear some varibales
activeNode = undefined;
let box = canvas?.getBoundingClientRect();
const mouseX = e.x - (box?.left ?? 0);
const mouseY = e.y - (box?.top ?? 0);
const wy = worldY(mouseY);
const wx = worldX(mouseX);
startMX = mouseX;
startMY = mouseY;
if (e.button === 2 || e.button === 1) {
tempMouseAction = mouseAction;
mouseAction = 'drag';
startX = x;
startY = y;
} else if (e.button === 0) {
if (linkMode) return;
// if inside a box allow clicks
for (const node of nodes) {
if (
wx >= node.x &&
wx <= node.x + node.width &&
wy <= node.y &&
wy >= node.y - node.height
) {
if (!linkMode && node.permission === 0) {
activeNode = node;
startX = x;
startY = y;
startMX = node.x - wx;
startMY = node.y - wy;
mouseAction = 'move';
}
return;
}
}
mouseAction = 'create';
curPosY = canvasY(Math.ceil(wy));
curPosX = canvasX(Math.floor(wx));
startMX = curPosX;
startMY = curPosY;
}
e.preventDefault();
e.stopPropagation();
}
async function onmouseup(e: MouseEvent) {
//
// Create
//
// Maybe don't do this it's super bad
while (mouseAction === 'create') {
let width = Math.abs(startMX - curPosX) / grid_size;
let height = Math.abs(startMY - curPosY) / grid_size;
if (width <= 8 || height <= 4) break;
try {
const result = await post('user/status/node', {
id: '',
user_id: '',
x: worldX(Math.min(startMX, curPosX)),
y: worldY(Math.min(startMY, curPosY)),
height,
width,
icon: icon_list[Math.floor(Math.random() * icon_list.length)],
name: 'New Status',
permission: 0
} as Node);
nodes.push(result);
// Tell svelte that nodes is updated
nodes = nodes;
} catch (e) {
console.log('TODO inform user', e);
}
break;
}
//
// Move
//
if (mouseAction === 'move' && activeNode) {
try {
await put('user/status/node', activeNode);
} catch (e) {
console.log('TODO: inform user', e);
}
activeNode = undefined;
}
//
// Ohter
//
if (mouseAction === 'drag' && tempMouseAction === 'link' && startX === x && startY === y) {
mouseAction = undefined;
tempMouseAction = undefined;
linkSource = undefined;
startX = 0;
startY = 0;
startMX = 0;
startMY = 0;
}
if (mouseAction !== 'link' && mouseAction) {
mouseAction = tempMouseAction;
tempMouseAction = undefined;
startX = 0;
startY = 0;
startMX = 0;
startMY = 0;
}
}
// if the mouse leaves the area clear all the stuffs
function onmouseleave(e: MouseEvent) {
if (mouseAction !== 'link') {
mouseAction = undefined;
startX = 0;
startY = 0;
startMX = 0;
startMY = 0;
}
}
function onmousemove(e: MouseEvent) {
let box = canvas?.getBoundingClientRect();
let mouseX = e.x - (box?.left ?? 0);
let mouseY = e.y - (box?.top ?? 0);
if (mouseAction === 'drag') {
x = startX + (startMX - mouseX);
y = startY + (startMY - mouseY);
} else if (mouseAction === 'create') {
const wy = worldY(mouseY);
const wx = worldX(mouseX);
curPosY = canvasY(Math.floor(wy));
curPosX = canvasX(Math.ceil(wx));
} else if (mouseAction === 'move' && activeNode) {
const wy = worldY(mouseY);
const wx = worldX(mouseX);
activeNode.x = Math.floor(wx + startMX);
activeNode.y = Math.ceil(wy + startMY);
}
if (mouseAction === 'link' || tempMouseAction === 'link') {
curPosX = mouseX;
curPosY = mouseY;
}
}
/*
* For now disable the right click menu
*/
function oncontextmenu(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
}
/*
* Link stuff
*/
let linkSource: LinkNode | undefined = $state();
//
//
// Utils
//
//
function canvasX(inX: number) {
let box = canvas?.getBoundingClientRect();
return inX * grid_size + Math.round((box?.width ?? 0) / 2) - x;
}
function canvasY(inY: number) {
let box = canvas?.getBoundingClientRect();
return Math.round((box?.height ?? 0) / 2) - y - inY * grid_size;
}
function worldX(canvasX: number) {
let box = canvas?.getBoundingClientRect();
return (canvasX + x - Math.round((box?.width ?? 0) / 2)) / grid_size;
}
function worldY(canvasY: number) {
let box = canvas?.getBoundingClientRect();
return (Math.round((box?.height ?? 0) / 2) - y - canvasY) / grid_size;
}
</script>
<div class="flex flex-col h-[100vh]">
<NavBar />
<div class="w-full flex-grow p-5 px-7">
<div
class="bg-white rounded-lg w-full h-full grid relative overflow-hidden"
style="--grid-size: {grid_size}px; cursor: {mouseAction === 'move' ? 'grab' : 'auto'};"
role="none"
bind:this={canvas}
{onmousedown}
{onmouseup}
{oncontextmenu}
{onmousemove}
{onmouseleave}
>
<!-- Background grid -->
<!-- x -->
<div class="absolute background" style="--offset-y: {-y + initialGridOffsetY}px;"></div>
<!-- y -->
<div
class="absolute background"
style="--grid-angle: 90deg; --offset-x: {-x + initialGridOffsetX}px;"
></div>
<!-- Control buttons -->
<div class="absolute left-5 top-5 flex gap-5 z-50">
<!-- Reset view port -->
{#if x !== 0 || y !== 0}
<button
class="bg-white p-2 shadow rounded-lg shadow-violet-500 h-[40px] w-[40px] text-violet-500"
onclick={() => {
x = 0;
y = 0;
}}
>
<span class="bi bi-arrow-clockwise"></span>
</button>
{/if}
<!-- Enable link mode -->
<button
class="p-2 shadow rounded-lg shadow-violet-500 h-[40px] w-[40px] text-violet-500 {linkMode
? 'bg-blue-100'
: 'bg-white'}"
onclick={() => {
linkMode = !linkMode;
linkSource = undefined;
}}
>
<span class="bi bi-link-45deg"></span>
</button>
</div>
<!--
-
- Links
-
-->
<!-- Create link -->
{#if linkSource}
<Link
x1={extractLinkNodePosX(canvasX, linkSource, grid_size)}
y1={extractLinkNodePosY(canvasY, linkSource, grid_size)}
x2={curPosX}
y2={curPosY}
/>
{/if}
<!-- Existing nodes -->
{#each links as _, i}
{@const sourceN = links[i].sourceNode}
{@const targetN = links[i].targetNode}
<Link
x1={extractLinkNodePosX(canvasX, sourceN, grid_size)}
y1={extractLinkNodePosY(canvasY, sourceN, grid_size)}
x2={extractLinkNodePosX(canvasX, targetN, grid_size)}
y2={extractLinkNodePosY(canvasY, targetN, grid_size)}
/>
{/each}
<!--
-
- Nodes
-
-->
<!-- Create rectangle -->
{#if mouseAction === 'create'}
<Rect x1={startMX} y1={startMY} x2={curPosX} y2={curPosY}>
{#if Math.abs(startMX - curPosX) / grid_size <= 8 || Math.abs(startMY - curPosY) / grid_size <= 4}
<div class="bg-red-400 bg-opacity-40 rounded-lg w-full h-full"></div>
{:else}
<div class="bg-green-400 bg-opacity-40 rounded-lg w-full h-full"></div>
{/if}
</Rect>
{/if}
<!-- Time to do some fun shit here -->
{#each nodes as _, i}
<NodeRect
bind:node={nodes[i]}
{canvasX}
{canvasY}
{grid_size}
{mouseAction}
{linkMode}
linkSource={nodes[i] === linkSource?.node ? linkSource : undefined}
onremove={async () => {
try {
await deleteR(`user/status/node/${nodes[i].id}`);
links = links.filter(link => link.sourceNode.node.id !== nodes[i].id && link.targetNode.node.id !== nodes[i].id)
nodes.splice(i, 1);
nodes = nodes;
} catch (e) {
console.log("TODO inform the user", e)
}
}}
onNodePointClick={async (inNodeX, inNodeY) => {
if (mouseAction === undefined) {
linkSource = {
node: nodes[i],
x: inNodeX,
y: inNodeY
};
mouseAction = 'link';
} else if (mouseAction === 'link' && linkSource) {
if (nodes[i] === linkSource.node) {
linkSource = undefined;
return;
}
try {
await put('user/status/link', {
id: '',
user_id: '',
source_node: linkSource.node.id,
target_node: nodes[i].id,
bi: false,
source_x: linkSource.x,
source_y: linkSource.y,
target_x: inNodeX,
target_y: inNodeY
} as FullLinkApi);
// if mouse action is link then we can assume that linkSource is set
links.push({
sourceNode: { ...linkSource },
targetNode: {
node: nodes[i],
x: inNodeX,
y: inNodeY
},
bi: false
});
// Tell svelte that the links changed
links = links;
linkSource = undefined;
mouseAction = undefined;
} catch (e) {
console.log('inform the user', e);
}
}
}}
/>
{/each}
</div>
</div>
</div>
<style>
:root {
--grid-color: #c1c1c1;
--grid-size: 35px;
--border-size: 1px;
--grid-angle: 0deg;
--offset-x: 0px;
--offset-y: 0px;
}
.background {
top: 0;
left: 0;
right: -1px;
bottom: -1px;
background: repeating-linear-gradient(
var(--grid-angle),
transparent,
transparent calc(var(--grid-size) - var(--border-size)),
var(--grid-color) calc(var(--grid-size) - var(--border-size)),
var(--grid-color) var(--grid-size)
);
background-position: var(--offset-x) var(--offset-y);
background-size: var(--grid-size) var(--grid-size);
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import icon_list from './icons-list.json';
let { dialog = $bindable(), onselect }: { dialog?: HTMLDialogElement, onselect?: (text: string) => void } = $props();
let filter = $state('');
</script>
<dialog bind:this={dialog}>
<div class="bg-white rounded-lg">
<div>
<input class="finput" bind:value={filter} />
</div>
<div class="w-[60vw] max-w-[60vw] h-[60vh] max-h-[60vh]">
<div class="grid grid-flow-row p-3 gap-2 w-[60vw] max-w-[60vw]" style="grid-template-columns: repeat(auto-fit, minmax(62px, 1fr))">
{#each filter ? icon_list.filter((a) => {
return a.match(new RegExp(filter, 'ig'));
}) : icon_list as icon}
<button
class="text-violet-500 p-4 text-3xl shadow rounded-lg leading-none min-h-0 w-[62px] h-[62px]"
onclick={() => {
onselect?.(icon);
dialog?.close();
}}
>
<span class="bi bi-{icon}"> </span>
</button>
{/each}
</div>
<div class="p-4 w-full"></div>
</div>
</div>
</dialog>

View File

@ -0,0 +1,78 @@
<script lang="ts">
let {
x1,
x2,
y1,
y2
}: {
x1?: number;
x2?: number;
y1?: number;
y2?: number;
} = $props();
let x = $state(0);
let y = $state(0);
let width = $state(0);
let height = $state(0);
let path = $state('');
const padding = 20;
$effect(() => {
if (x1 !== undefined && x2 !== undefined && y1 !== undefined && y2 !== undefined) {
const xMin = Math.min(x1, x2);
const yMin = Math.min(y1, y2);
x = xMin;
width = Math.abs(x1 - x2);
y = yMin;
height = Math.abs(y1 - y2);
const w = width;
const h = height;
const p = padding;
// Start on bottom left conner
if (xMin !== x1 && yMin === y1) {
path = `M ${p} ${h + p} Q ${w * 0.1 + p} ${h * 0.4 + p}, ${w / 2 + p} ${h / 2 + p} T ${w + p} ${p}`;
// Start on bottom right conner
} else if (xMin === x1 && yMin === y1) {
path = `M ${w + p} ${h + p} Q ${w * 0.9 + p} ${h * 0.4 + p}, ${w / 2 + p} ${h / 2 + p} T ${p} ${p}`;
// Start on top left conner
} else if (xMin !== x1 && yMin !== y1) {
path = `M ${p} ${p} Q ${w * 0.1 + p} ${h * 0.6 + p}, ${w / 2 + p} ${h / 2 + p} T ${w + p} ${h + p}`;
// Start on top right conner
} else if (xMin === x1 && yMin !== y1) {
path = `M ${w + p} ${p} Q ${w * 0.9 + p} ${h * 0.6 + p}, ${w / 2 + p} ${h / 2 + p} T ${p} ${h + p}`;
}
}
});
</script>
<svg
viewBox="0 0 {width + padding * 2} {height + padding * 2}"
class="absolute"
width={width + padding * 2}
height={height + padding * 2}
style="width: {width + padding * 2}px; height: {height + padding * 2}px; top: {y -
padding}px; left: {x - padding}px"
>
<marker
id="arrow"
viewBox="0 0 10 10"
refX="5"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<path d={path} stroke="black" stroke-width="2" fill="transparent" marker-start="url(#arrow)" />
</svg>

View File

@ -0,0 +1,162 @@
<script lang="ts">
import { put } from '$lib/utils';
import IconPicker from './IconPicker.svelte';
import Rect from './Rect.svelte';
import type { Node, ActionType, LinkNode } from './types';
//
// consts
//
const connectionPointSize = 10;
//
//
//
let hover = $state(false);
let iconSelector: HTMLDialogElement;
let {
node = $bindable(),
grid_size,
canvasX,
canvasY,
mouseAction,
onremove,
linkMode,
onNodePointClick,
linkSource
}: {
node: Node;
grid_size: number;
canvasX: (x: number) => number;
canvasY: (y: number) => number;
mouseAction: ActionType;
onremove: () => void;
linkMode: boolean;
onNodePointClick: (x: number, y: number) => void;
linkSource: LinkNode | undefined;
} = $props();
let cx = $derived(canvasX(node.x));
let cy = $derived(canvasY(node.y));
async function save() {
try {
const r = await put('user/status/node', node);
node = r;
} catch (e) {
console.log('TODO inform the user', e);
}
}
</script>
<Rect x={cx - 1} y={cy - 1} height={node.height * grid_size + 1} width={node.width * grid_size + 1}>
<div
class="bg-white shadow-lg border-slate-300 border-solid border rounded-lg w-full h-full flex flex-col justify-center items-center gap-2 relative"
role="none"
onmouseenter={() => {
if (mouseAction === undefined || mouseAction === 'link') {
hover = true;
}
}}
onmouseleave={() => {
if (hover) {
hover = false;
}
}}
>
<button
disabled={node.permission !== 0}
onclick={() => {
iconSelector.showModal();
}}
>
<h1>
{#if node.icon}
<span class="bi bi-{node.icon}"></span>
{:else}
Select an icon
{/if}
</h1>
</button>
<div class="p-2">
{#if node.permission === 0}
<input bind:value={node.name} class="finput text-center" onblur={save} />
{:else}
{node.name}
{/if}
</div>
{#if node.permission === 0 && !linkMode}
<button
class="bg-red-400 hover:bg-red-800 -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] absolute text-center text-white leading-none"
onclick={onremove}
>
<div class="bi bi-x mt-[2px]"></div>
</button>
{/if}
</div>
<!--
Node connections ponints
-->
{#if hover && linkMode}
<!-- Top -->
{#each { length: node.width } as _, i}
{@const adjust = grid_size / 2 + grid_size * i - connectionPointSize / 2}
<button
class="{linkSource?.x === i + 1 && linkSource?.y === 0
? 'bg-red-200 hover:bg-red-800'
: 'bg-blue-200 hover:bg-blue-800'} rounded-full absolute"
style="top: 0px; left: {adjust}px; width: {connectionPointSize}px; height: {connectionPointSize}px;"
onclick={() => onNodePointClick(i, -1)}
></button>
{/each}
<!-- Bottom -->
{#each { length: node.width } as _, i}
{@const adjust = grid_size / 2 + grid_size * i - connectionPointSize / 2}
<button
class="{linkSource?.x === i + 1 && linkSource?.y === node.height
? 'bg-red-200 hover:bg-red-800'
: 'bg-blue-200 hover:bg-blue-800'} rounded-full absolute"
style="bottom: 0px; left: {adjust}px; width: {connectionPointSize}px; height: {connectionPointSize}px;"
onclick={() => onNodePointClick(i, node.height)}
></button>
{/each}
<!-- Left -->
{#each { length: node.height } as _, i}
{@const adjust = grid_size / 2 + grid_size * i - connectionPointSize / 2}
<button
class="{linkSource?.x === 0 && linkSource?.y === i + 1
? 'bg-red-200 hover:bg-red-800'
: 'bg-blue-200 hover:bg-blue-800'} rounded-full absolute"
style="left: 0px; top: {adjust}px; width: {connectionPointSize}px; height: {connectionPointSize}px;"
onclick={() => onNodePointClick(-1, i)}
></button>
{/each}
<!-- Right -->
{#each { length: node.height } as _, i}
{@const adjust = grid_size / 2 + grid_size * i - connectionPointSize / 2}
<button
class="{linkSource?.x === node.width && linkSource?.y === i + 1
? 'bg-red-200 hover:bg-red-800'
: 'bg-blue-200 hover:bg-blue-800'} rounded-full absolute"
style="right: 0px; top: {adjust}px; width: {connectionPointSize}px; height: {connectionPointSize}px;"
onclick={() => onNodePointClick(node.width, i)}
></button>
{/each}
{/if}
</Rect>
<IconPicker
bind:dialog={iconSelector}
onselect={(text) => {
node.icon = text;
window.requestAnimationFrame(() => {
save();
});
}}
/>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
x1,
x2,
y1,
y2,
x,
y,
width,
height,
children
}: {
x1?: number;
x2?: number;
y1?: number;
y2?: number;
x?: number;
y?: number;
width?: number;
height?: number;
children: Snippet;
} = $props();
$effect(() => {
if (x1 !== undefined && x2 !== undefined && y1 !== undefined && y2 !== undefined) {
const xMin = Math.min(x1, x2);
const yMin = Math.min(y1, y2);
x = xMin;
width = Math.abs(x1 - x2);
y = yMin;
height = Math.abs(y1 - y2);
}
});
</script>
<div class="absolute" style="left: {x}px; top: {y}px; width: {width}px; height: {height}px;">
{@render children()}
</div>

View File

@ -0,0 +1,5 @@
import fs from 'fs';
const icons_list = fs.readdirSync('../../../node_modules/bootstrap-icons/icons').map(a => a.replace('.svg', ''));
fs.writeFileSync("icons-list.json", JSON.stringify(icons_list))

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,60 @@
export type Node = {
id: string,
user_id: string,
x: number;
y: number;
width: number;
height: number;
name: string;
icon: string;
// 1 disables editing
permission: number;
};
export type LinkNode = {
node: Node;
x: number;
y: number;
}
export type FullLink = {
sourceNode: LinkNode;
targetNode: LinkNode;
bi?: boolean;
}
export type FullLinkApi = {
id: string,
user_id: string,
target_node: string | null,
source_node: string | null,
bi: boolean,
source_x: number,
source_y: number,
target_x: number,
target_y: number
}
export type ActionType = undefined | 'drag' | 'create' | 'move' | 'link';
export function extractLinkNodePosX(canvasX: (x: number) => number, node: LinkNode, grid_size: number) {
if (node.x === -1) {
return canvasX(node.node.x);
}
if (node.x === node.node.width) {
return canvasX(node.node.x) + node.node.width * grid_size;
}
return canvasX(node.node.x) + node.x * grid_size + grid_size / 2;
}
export function extractLinkNodePosY(canvasY: (x: number) => number, node: LinkNode, grid_size: number) {
if (node.y === -1) {
return canvasY(node.node.y);
}
if (node.y === node.node.height) {
return canvasY(node.node.y) + node.node.height * grid_size;
}
return canvasY(node.node.y) + node.y * grid_size + grid_size / 2;
}

View File

@ -16,6 +16,8 @@
let showLinked = $state(false); let showLinked = $state(false);
let showToApply = $state(false); let showToApply = $state(false);
let totalPercetange = $state(true);
// Handle the graph creation // Handle the graph creation
$effect(() => { $effect(() => {
if (!chartDiv || applications.length == 0) return; if (!chartDiv || applications.length == 0) return;
@ -46,6 +48,8 @@
Linkedin: 0 Linkedin: 0
}; };
let count = 0;
applications.forEach((a) => { applications.forEach((a) => {
let source: NodeType; let source: NodeType;
@ -65,6 +69,8 @@
return; return;
} }
count++;
if (a.url.includes('linkedin')) { if (a.url.includes('linkedin')) {
source = 'Linkedin'; source = 'Linkedin';
sourceData['Linkedin'] += 1; sourceData['Linkedin'] += 1;
@ -106,6 +112,12 @@
return; return;
} }
} }
if (history.includes(`${ApplicationStatus.TasksToDo2}`)) {
source = addGraph(source, ApplicationStatus.TasksToDo2);
if (a.status == ApplicationStatus.TasksToDo2) {
return;
}
}
if (history.includes(`${ApplicationStatus.InterviewStep1}`)) { if (history.includes(`${ApplicationStatus.InterviewStep1}`)) {
source = addGraph(source, ApplicationStatus.InterviewStep1); source = addGraph(source, ApplicationStatus.InterviewStep1);
if (a.status == ApplicationStatus.InterviewStep1) { if (a.status == ApplicationStatus.InterviewStep1) {
@ -118,6 +130,12 @@
return; return;
} }
} }
if (history.includes(`${ApplicationStatus.FinalInterview}`)) {
source = addGraph(source, ApplicationStatus.FinalInterview);
if (a.status == ApplicationStatus.FinalInterview) {
return;
}
}
addGraph(source, ApplicationStatus.ApplyedButSaidNo); addGraph(source, ApplicationStatus.ApplyedButSaidNo);
}); });
@ -154,7 +172,9 @@
originalValue: node, originalValue: node,
id: name, id: name,
index: i, index: i,
percentage: Math.trunc((value / applications.length) * 100) percentage: Math.trunc(
(value / (totalPercetange ? applications.length : count)) * 100
)
}; };
return base; return base;
}); });
@ -201,9 +221,9 @@
// let color = d3.schemeSpectral[nodes.length]; // let color = d3.schemeSpectral[nodes.length];
// let color = d3.interpolateTurbo(nodes.length); // let color = d3.interpolateTurbo(nodes.length);
function getColor(index: number) { function getColor(index: number) {
return d3.interpolateRainbow(index/nodes.length); return d3.interpolateRainbow(index / nodes.length);
} }
// add in the links // add in the links
var link = svg var link = svg
@ -221,10 +241,12 @@
.attr('class', 'link') .attr('class', 'link')
.attr('d', path) .attr('d', path)
.style('stroke', function (d) { .style('stroke', function (d) {
return d3.rgb( return d3
getColor(d.source.index) .rgb(
// color[d.source.index] getColor(d.source.index)
).toString(); // color[d.source.index]
)
.toString();
}) })
.style('stroke-width', function (d) { .style('stroke-width', function (d) {
return Math.max(1, d.dy); return Math.max(1, d.dy);
@ -264,13 +286,16 @@
.attr('width', sankey.getNodeWidth()) .attr('width', sankey.getNodeWidth())
.style('fill', function (d) { .style('fill', function (d) {
return getColor(d.index); return getColor(d.index);
//color[d.index]; //color[d.index];
}) })
.style('stroke', (d) => { .style('stroke', (d) => {
return d3.rgb( return d3
getColor(d.index) .rgb(
//color[d.index] getColor(d.index)
).darker(2).toString(); //color[d.index]
)
.darker(2)
.toString();
}) })
.append('title') .append('title')
.text(function (d) { .text(function (d) {
@ -329,6 +354,10 @@
<label for="showToApply">Show To Apply</label> <label for="showToApply">Show To Apply</label>
<input id="showToApply" type="checkbox" bind:checked={showToApply} /> <input id="showToApply" type="checkbox" bind:checked={showToApply} />
</fieldset> </fieldset>
<fieldset>
<label for="totalPercetange">Total percentage</label>
<input id="totalPercetange" type="checkbox" bind:checked={totalPercetange} />
</fieldset>
</div> </div>
</div> </div>
<div class="flex flex-grow flex-col p-5"> <div class="flex flex-grow flex-col p-5">

View File

@ -0,0 +1,28 @@
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
let { save, company = $bindable() }: { company: string; save: () => void } = $props();
let companies = $derived(new Set(applicationStore.all.map((a) => a.company)));
let fcomps = $derived(
company === ''
? []
: [...companies.values()].filter((a) => {
// TODO improve this a lot I want to make like matching algo
return a.match(company) && a !== company;
})
);
</script>
<fieldset class="grow">
<label class="flabel" for="title">Company</label>
<input class="finput" id="title" bind:value={company} onchange={save} />
<div class="flex gap-2 flex-wrap">
{#each fcomps as comp}
<button class="bg-blue-200 px-3 p-1 min-h-0 rounded-lg" onclick={() => company = comp}>
{comp}
</button>
{/each}
</div>
</fieldset>

View File

@ -0,0 +1,198 @@
<script lang="ts">
import {
ApplicationStatus,
applicationStore,
type Application,
type AsEnum
} from '$lib/ApplicationsStore.svelte';
import { deleteR, put } from '$lib/utils';
import DropZone from './DropZone.svelte';
let { activeItem = $bindable() }: { activeItem: Application | undefined } = $props();
async function moveStatus(status: AsEnum<typeof ApplicationStatus>, moveOut = true) {
if (!activeItem) return;
// Deactivate active item
try {
await put('application/status', {
id: activeItem.id,
status: status
});
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
applicationStore.loadAplyed(true);
if (moveOut) {
activeItem = undefined;
} else {
activeItem.status = status;
}
//openedWindow?.close();
//openedWindow = undefined;
}
async function remove() {
if (!activeItem) return;
// Deactivate active item
try {
await deleteR(`application/${activeItem.id}`);
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
activeItem = undefined;
//openedWindow?.close();
//openedWindow = undefined;
}
</script>
{#if applicationStore.dragging && activeItem}
<div
class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white"
>
<!-- Do nothing -->
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<DropZone
icon="box-arrow-down"
ondrop={async () => {
await moveStatus(ApplicationStatus.ToApply);
applicationStore.loadAplyed(true);
applicationStore.loadAll(true);
}}
>
To apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Ignore -->
<DropZone icon="trash-fill" ondrop={() => moveStatus(ApplicationStatus.Ignore)}>
Ignore it
</DropZone>
{/if}
{#if ([ApplicationStatus.WorkingOnIt, ApplicationStatus.Expired] as number[]).includes(activeItem.status)}
<!-- Expired -->
<DropZone
icon="clock-fill text-orange-500"
ondrop={() => {
if (activeItem && activeItem.status === ApplicationStatus.Expired) {
moveStatus(ApplicationStatus.ToApply, false);
} else {
moveStatus(ApplicationStatus.Expired);
}
}}
>
Mark as expired
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Repeated -->
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}>Delete it</DropZone>
{/if}
{#if ([ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo] as number[]).includes(activeItem.status)}
<!-- Applyed -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.Applyed);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.Applyed}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.TasksToDo}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo2);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do 2
</DropZone>
{/if}
{#if ([ApplicationStatus.TasksToDo, ApplicationStatus.Applyed] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep1);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 1
</DropZone>
{/if}
{#if ([ApplicationStatus.InterviewStep1] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep2);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 2
</DropZone>
{/if}
{#if ([ApplicationStatus.InterviewStep1, ApplicationStatus.InterviewStep2] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="badge-vo-fill text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.FinalInterview);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview Final
</DropZone>
{/if}
{#if !([ApplicationStatus.ApplyedButSaidNo] as number[]).includes(activeItem.status)}
<!-- Rejected -->
<DropZone
icon="fire text-danger"
ondrop={() => {
moveStatus(ApplicationStatus.ApplyedButSaidNo);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
I was rejeted :(
</DropZone>
{/if}
</div>
{/if}

View File

@ -27,8 +27,11 @@
async function submit(item: Application) { async function submit(item: Application) {
try { try {
application.linked_application = item.id; application.linked_application = item.id;
application.status = ApplicationStatus.LinkedApplication;
await put('application/update', application); await put('application/update', application);
await put('application/status', {
id: application.id,
status: ApplicationStatus.LinkedApplication
});
dialog.close(); dialog.close();
onreload(item); onreload(item);
} catch (e) { } catch (e) {
@ -70,9 +73,9 @@
</div> </div>
{/if} {/if}
</h2> </h2>
<span class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full"> <span>
{item.url} {ApplicationStatusMaping[item.status]}
</span> </span>
</button> </button>
</div> </div>
{/each} {/each}

View File

@ -36,18 +36,9 @@
document.removeEventListener('keydown', docKey); document.removeEventListener('keydown', docKey);
}; };
}); });
</script>
<dialog class="card max-w-[50vw]" bind:this={dialogElement}> let internal = $derived(
<div class="flex"> applications.filter((i) => {
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{applications.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
<!-- TODO loading screen -->
{#each applications.filter((i) => {
if (application && i.id == application.id) { if (application && i.id == application.id) {
return false; return false;
} }
@ -71,7 +62,20 @@
} }
return x.match(f); return x.match(f);
}) as item} })
);
</script>
<dialog class="card max-w-[50vw]" bind:this={dialogElement}>
<div class="flex">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{internal.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
<!-- TODO loading screen -->
{#each internal as item}
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none"> <div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
<button <button
class="text-left max-w-full" class="text-left max-w-full"
@ -90,14 +94,9 @@
</div> </div>
{/if} {/if}
</div> </div>
<div>
{ApplicationStatusMaping[item.status]}
</div>
</h2> </h2>
<span <span>
class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full" {ApplicationStatusMaping[item.status]}
>
{item.url}
</span> </span>
</button> </button>
</div> </div>

View File

@ -8,7 +8,7 @@
ApplicationStatusMaping ApplicationStatusMaping
} from '$lib/ApplicationsStore.svelte'; } from '$lib/ApplicationsStore.svelte';
let { application }: { application: Application } = $props(); let { application, showAll }: { application: Application, showAll: boolean } = $props();
let events: (ApplicationEvent & { timeDiff: string })[] = $state([]); let events: (ApplicationEvent & { timeDiff: string })[] = $state([]);
@ -58,7 +58,7 @@
if (event.event_type === EventType.Creation) { if (event.event_type === EventType.Creation) {
status = ApplicationStatus.ToApply; status = ApplicationStatus.ToApply;
} }
if (event.event_type !== EventType.StatusUpdate) { if (event.event_type !== EventType.StatusUpdate || showAll) {
if (time) { if (time) {
_events[_events.length - 1].timeDiff = time; _events[_events.length - 1].timeDiff = time;
} }
@ -118,10 +118,13 @@
class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-10" class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-10"
> >
{#if event.event_type == EventType.Creation} {#if event.event_type == EventType.Creation}
<span class="bi bi-plus"></span> <span
title={`Created @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-plus"
></span>
{:else if event.event_type == EventType.View} {:else if event.event_type == EventType.View}
<span <span
title="Views {new Date(event.time).toLocaleString()}" title={`Viewed @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-eye" class="bi bi-eye"
></span> ></span>
{:else} {:else}

View File

@ -3,21 +3,21 @@
ApplicationStatus, ApplicationStatus,
ApplicationStatusMaping, ApplicationStatusMaping,
applicationStore, applicationStore,
type Application, type Application
type AsEnum
} from '$lib/ApplicationsStore.svelte'; } from '$lib/ApplicationsStore.svelte';
import { put, preventDefault, post, get, deleteR } from '$lib/utils'; import { put, preventDefault, post, get } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ExtractTextDialog from './ExtractTextDialog.svelte'; import ExtractTextDialog from './ExtractTextDialog.svelte';
import Flair from '../flair/Flair.svelte'; import Flair from '../flair/Flair.svelte';
import NewUrlDialog from './NewUrlDialog.svelte'; import NewUrlDialog from './NewUrlDialog.svelte';
import DropZone from './DropZone.svelte';
import { userStore } from '$lib/UserStore.svelte'; import { userStore } from '$lib/UserStore.svelte';
import LinkApplication from './LinkApplication.svelte'; import LinkApplication from './LinkApplication.svelte';
import SearchApplication from './SearchApplication.svelte'; import SearchApplication from './SearchApplication.svelte';
import NewApplication from './NewApplication.svelte'; import NewApplication from './NewApplication.svelte';
import Timeline from './Timeline.svelte'; import Timeline from './Timeline.svelte';
import DropingZone from './DropingZone.svelte';
import CompanyField from './CompanyField.svelte';
let activeItem: Application | undefined = $state(); let activeItem: Application | undefined = $state();
@ -30,6 +30,8 @@
let showExtraData = $state(false); let showExtraData = $state(false);
let drag = $state(true);
async function activate(item?: Application) { async function activate(item?: Application) {
if (!item) { if (!item) {
return; return;
@ -94,6 +96,9 @@
); );
} }
//
// Make the CV open on a new page
//
function docKey(e: KeyboardEvent) { function docKey(e: KeyboardEvent) {
if (activeItem && e.ctrlKey && e.code === 'KeyO') { if (activeItem && e.ctrlKey && e.code === 'KeyO') {
openCV(activeItem.id); openCV(activeItem.id);
@ -108,6 +113,9 @@
document.removeEventListener('keydown', docKey); document.removeEventListener('keydown', docKey);
}; };
}); });
//
//
//
async function loadActive() { async function loadActive() {
try { try {
@ -175,46 +183,6 @@
applicationStore.loadItem = undefined; applicationStore.loadItem = undefined;
}); });
async function moveStatus(status: AsEnum<typeof ApplicationStatus>, moveOut = true) {
if (!activeItem) return;
// Deactivate active item
try {
await put('application/status', {
id: activeItem.id,
status: status
});
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
applicationStore.loadAplyed(true);
if (moveOut) {
activeItem = undefined;
} else {
activeItem.status = status;
}
//openedWindow?.close();
//openedWindow = undefined;
}
async function remove() {
if (!activeItem) return;
// Deactivate active item
try {
await deleteR(`application/${activeItem.id}`);
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
activeItem = undefined;
//openedWindow?.close();
//openedWindow = undefined;
}
async function save() { async function save() {
try { try {
await put('application/update', activeItem); await put('application/update', activeItem);
@ -232,14 +200,6 @@
console.log('info data', e); console.log('info data', e);
} }
} }
let drag = $state(true);
const statusMapping: string = $derived(
(ApplicationStatusMaping[
activeItem?.status as (typeof ApplicationStatus)[keyof typeof ApplicationStatus]
] as string) ?? ''
);
</script> </script>
<div class="flex flex-col w-full gap-2 min-w-0 relative" role="none"> <div class="flex flex-col w-full gap-2 min-w-0 relative" role="none">
@ -260,7 +220,9 @@
<div class="w-full"> <div class="w-full">
{#if activeItem.status != 1} {#if activeItem.status != 1}
<div class="bg-danger text-white p-2 rounded-lg text-lg font-bold"> <div class="bg-danger text-white p-2 rounded-lg text-lg font-bold">
{statusMapping} {(ApplicationStatusMaping[
activeItem?.status as (typeof ApplicationStatus)[keyof typeof ApplicationStatus]
] as string) ?? ''}
</div> </div>
{/if} {/if}
{#if showExtraData} {#if showExtraData}
@ -278,15 +240,7 @@
</fieldset> </fieldset>
{/if} {/if}
<div class="flex gap-2"> <div class="flex gap-2">
<fieldset class="grow"> <CompanyField bind:company={activeItem.company} {save} />
<label class="flabel" for="title">Company</label>
<input
class="finput"
id="title"
bind:value={activeItem.company}
onchange={save}
/>
</fieldset>
<fieldset class="grow"> <fieldset class="grow">
<label class="flabel" for="title">Recruiter</label> <label class="flabel" for="title">Recruiter</label>
<input <input
@ -296,6 +250,16 @@
onchange={save} onchange={save}
/> />
</fieldset> </fieldset>
<fieldset>
<label class="flabel" for="title">Agency</label>
<input
class="finput"
id="title"
type="checkbox"
bind:checked={activeItem.agency}
onchange={save}
/>
</fieldset>
</div> </div>
<fieldset> <fieldset>
<label class="flabel" for="title">Title</label> <label class="flabel" for="title">Title</label>
@ -316,7 +280,7 @@
/> />
</fieldset> </fieldset>
{#if !activeItem.unique_url || showExtraData} {#if !activeItem.unique_url || showExtraData}
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden"> <fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Url</div> <div class="flabel">Url</div>
<div class="finput bg-white w-full break-keep"> <div class="finput bg-white w-full break-keep">
{activeItem.url} {activeItem.url}
@ -324,7 +288,7 @@
</fieldset> </fieldset>
{/if} {/if}
{#if activeItem.unique_url} {#if activeItem.unique_url}
<fieldset draggable="false"> <fieldset>
<div class="flabel">Unique Url</div> <div class="flabel">Unique Url</div>
<div class="finput bg-white"> <div class="finput bg-white">
{activeItem.unique_url} {activeItem.unique_url}
@ -332,7 +296,7 @@
</fieldset> </fieldset>
{/if} {/if}
{#if activeItem.linked_application} {#if activeItem.linked_application}
<fieldset draggable="false"> <fieldset>
<div class="flabel">Linked Application</div> <div class="flabel">Linked Application</div>
<div class="finput bg-white"> <div class="finput bg-white">
{activeItem.linked_application} {activeItem.linked_application}
@ -340,7 +304,7 @@
</fieldset> </fieldset>
{/if} {/if}
{#if activeItem.application_time} {#if activeItem.application_time}
<fieldset draggable="false"> <fieldset>
<div class="flabel">Application Time</div> <div class="flabel">Application Time</div>
<div class="finput bg-white"> <div class="finput bg-white">
{activeItem.application_time} {activeItem.application_time}
@ -372,7 +336,6 @@
<fieldset> <fieldset>
<label class="flabel" for="extra">Message</label> <label class="flabel" for="extra">Message</label>
<textarea <textarea
draggable={false}
class="finput" class="finput"
id="extra" id="extra"
bind:value={activeItem.message} bind:value={activeItem.message}
@ -442,131 +405,9 @@
🔬 🔬
</button> </button>
</div> </div>
<Timeline application={activeItem} /> <Timeline application={activeItem} showAll={showExtraData} />
</div> </div>
{#if applicationStore.dragging} <DropingZone bind:activeItem />
<div
class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white"
>
<!-- Do nothing -->
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<DropZone
icon="box-arrow-down"
ondrop={() => {
moveStatus(ApplicationStatus.ToApply);
applicationStore.loadAplyed(true);
applicationStore.loadAll(true);
}}
>
To apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Ignore -->
<DropZone
icon="trash-fill"
ondrop={() => {
moveStatus(ApplicationStatus.Ignore);
}}
>
Ignore it
</DropZone>
{/if}
{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.Expired].includes(activeItem.status)}
<!-- Expired -->
<DropZone
icon="clock-fill text-orange-500"
ondrop={() => {
if (activeItem && activeItem.status === ApplicationStatus.Expired) {
moveStatus(ApplicationStatus.ToApply, false);
} else {
moveStatus(ApplicationStatus.Expired);
}
}}
>
Mark as expired
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Repeated -->
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}
>Delete it</DropZone
>
{/if}
{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo].includes(activeItem.status)}
<!-- Applyed -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.Applyed);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.Applyed}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do
</DropZone>
{/if}
{#if [ApplicationStatus.TasksToDo, ApplicationStatus.Applyed].includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep1);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 1
</DropZone>
{/if}
{#if [ApplicationStatus.InterviewStep1].includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep2);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 2
</DropZone>
{/if}
{#if ![ApplicationStatus.ApplyedButSaidNo].includes(activeItem.status)}
<!-- Rejected -->
<DropZone
icon="fire text-danger"
ondrop={() => {
moveStatus(ApplicationStatus.ApplyedButSaidNo)
applicationStore.loadAll(true);
}}
>
I was rejeted :(
</DropZone>
{/if}
</div>
{/if}
{:else} {:else}
<div <div
class="p-2 h-full w-full gap-2 flex-grow card grid place-items-center min-h-[50vh]" class="p-2 h-full w-full gap-2 flex-grow card grid place-items-center min-h-[50vh]"
@ -598,9 +439,9 @@
id={activeItem?.id ?? ''} id={activeItem?.id ?? ''}
bind:dialog={changeUrl} bind:dialog={changeUrl}
onreload={async (item) => { onreload={async (item) => {
item.events = await get(`events/${item.id}`); item.events = await get(`events/${item.id}`);
activeItem = item; activeItem = item;
}} }}
/> />
{/if} {/if}
@ -615,7 +456,7 @@
<SearchApplication <SearchApplication
application={activeItem} application={activeItem}
onreload={async (item) => { onreload={async (item) => {
item.events = await get(`events/${item.id}`); item.events = await get(`events/${item.id}`);
activeItem = item; activeItem = item;
}} }}
/> />