lot of changes
This commit is contained in:
parent
dd047c0bcf
commit
26301d1a13
@ -2,6 +2,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
spring.datasource.url=jdbc:postgresql://kronos.home:5432/applications
|
||||
spring.datasource.username=applications
|
||||
spring.datasource.password=applications
|
||||
spring-boot.run.jvmArguments=-Duser.timezone=UTC
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
# Disable the trace on the error responses
|
||||
|
@ -33,8 +33,10 @@ data class Application(
|
||||
var extra_data: String,
|
||||
var payrange: String,
|
||||
var status: Int,
|
||||
var status_id: String?,
|
||||
var company: String,
|
||||
var recruiter: String,
|
||||
var agency: Boolean,
|
||||
var message: String,
|
||||
var linked_application: String,
|
||||
var status_history: String,
|
||||
@ -56,8 +58,10 @@ data class Application(
|
||||
rs.getString("extra_data"),
|
||||
rs.getString("payrange"),
|
||||
rs.getInt("status"),
|
||||
rs.getString("status_id"),
|
||||
rs.getString("company"),
|
||||
rs.getString("recruiter"),
|
||||
rs.getBoolean("agency"),
|
||||
rs.getString("message"),
|
||||
rs.getString("linked_application"),
|
||||
rs.getString("status_history"),
|
||||
@ -85,6 +89,7 @@ data class CVData(
|
||||
val company: String,
|
||||
val recruiter: String,
|
||||
val message: String,
|
||||
val agency: Boolean,
|
||||
val flairs: List<SimpleFlair>
|
||||
)
|
||||
|
||||
@ -113,7 +118,7 @@ class ApplicationsController(
|
||||
|
||||
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 */
|
||||
@ -135,8 +140,10 @@ class ApplicationsController(
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
@ -263,8 +270,10 @@ class ApplicationsController(
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
@ -535,8 +544,9 @@ class ApplicationService(
|
||||
}
|
||||
|
||||
// Create time is auto created by the database
|
||||
// The default status is null
|
||||
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.url,
|
||||
application.original_url,
|
||||
@ -552,6 +562,7 @@ class ApplicationService(
|
||||
application.linked_application,
|
||||
application.status_history,
|
||||
application.application_time,
|
||||
application.agency,
|
||||
)
|
||||
|
||||
eventService.create(application.id, EventType.Creation)
|
||||
@ -595,7 +606,16 @@ class ApplicationService(
|
||||
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
|
||||
// TODO how status history works
|
||||
public fun updateStatus(application: Application): Application {
|
||||
|
||||
val status_string = "${application.status}"
|
||||
@ -627,7 +647,7 @@ class ApplicationService(
|
||||
public fun update(application: Application): Application {
|
||||
// I don't want ot update create_time
|
||||
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.original_url,
|
||||
application.unique_url,
|
||||
@ -639,6 +659,7 @@ class ApplicationService(
|
||||
application.recruiter,
|
||||
application.message,
|
||||
application.linked_application,
|
||||
application.agency,
|
||||
application.id,
|
||||
)
|
||||
return application
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -23,16 +23,20 @@ create table if not exists applications (
|
||||
status_history text default '',
|
||||
user_id text,
|
||||
extra_data text,
|
||||
-- this status will be deprecated in favor of the node style status
|
||||
status integer,
|
||||
status_id text default null,
|
||||
linked_application text default '',
|
||||
application_time text default '',
|
||||
agency boolean default false,
|
||||
create_time timestamp default now()
|
||||
);
|
||||
|
||||
-- Views are deprecated will be removed in the future
|
||||
create table if not exists views (
|
||||
id text primary key,
|
||||
application_id text not null,
|
||||
time timestamp default current_timestamp
|
||||
time timestamp default now()
|
||||
);
|
||||
|
||||
create table if not exists flair (
|
||||
@ -65,5 +69,41 @@ create table if not exists events (
|
||||
-- This only matters when event_type == 1
|
||||
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
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "^1.11.3"
|
||||
}
|
||||
}
|
22
pnpm-lock.yaml
Normal file
22
pnpm-lock.yaml
Normal 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: {}
|
@ -18,6 +18,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/node": "^22.8.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3": "^7.9.0",
|
||||
"eslint": "^9.0.0",
|
||||
@ -35,5 +36,8 @@
|
||||
"typescript-eslint": "^8.0.0-alpha.20",
|
||||
"vite": "^5.0.3"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "1.11.3"
|
||||
}
|
||||
}
|
||||
|
@ -7,25 +7,32 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
bootstrap-icons:
|
||||
specifier: 1.11.3
|
||||
version: 1.11.3
|
||||
devDependencies:
|
||||
'@sveltejs/adapter-auto':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
specifier: ^7.4.3
|
||||
version: 7.4.3
|
||||
'@types/eslint':
|
||||
specifier: ^8.56.7
|
||||
version: 8.56.10
|
||||
'@types/node':
|
||||
specifier: ^22.8.7
|
||||
version: 22.8.7
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
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)
|
||||
vite:
|
||||
specifier: ^5.0.3
|
||||
version: 5.3.3
|
||||
version: 5.3.3(@types/node@22.8.7)
|
||||
|
||||
packages:
|
||||
|
||||
@ -520,6 +527,9 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@22.8.7':
|
||||
resolution: {integrity: sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==}
|
||||
|
||||
'@types/pug@2.0.10':
|
||||
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
||||
|
||||
@ -651,6 +661,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bootstrap-icons@1.11.3:
|
||||
resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
|
||||
@ -1674,6 +1687,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
update-browserslist-db@1.1.0:
|
||||
resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
|
||||
hasBin: true
|
||||
@ -1964,18 +1980,18 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.18.0':
|
||||
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:
|
||||
'@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
|
||||
|
||||
'@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:
|
||||
'@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:
|
||||
'@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
|
||||
cookie: 0.6.0
|
||||
devalue: 5.0.0
|
||||
@ -1989,28 +2005,28 @@ snapshots:
|
||||
sirv: 2.0.4
|
||||
svelte: 5.0.0-next.174
|
||||
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:
|
||||
'@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
|
||||
svelte: 5.0.0-next.174
|
||||
vite: 5.3.3
|
||||
vite: 5.3.3(@types/node@22.8.7)
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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
|
||||
deepmerge: 4.3.1
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.10
|
||||
svelte: 5.0.0-next.174
|
||||
svelte-hmr: 0.16.0(svelte@5.0.0-next.174)
|
||||
vite: 5.3.3
|
||||
vitefu: 0.2.5(vite@5.3.3)
|
||||
vite: 5.3.3(@types/node@22.8.7)
|
||||
vitefu: 0.2.5(vite@5.3.3(@types/node@22.8.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -2144,6 +2160,10 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@22.8.7':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@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)':
|
||||
@ -2289,6 +2309,8 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bootstrap-icons@1.11.3: {}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@ -3330,6 +3352,8 @@ snapshots:
|
||||
|
||||
typescript@5.5.3: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
update-browserslist-db@1.1.0(browserslist@4.23.1):
|
||||
dependencies:
|
||||
browserslist: 4.23.1
|
||||
@ -3342,17 +3366,18 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite@5.3.3:
|
||||
vite@5.3.3(@types/node@22.8.7):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.4.39
|
||||
rollup: 4.18.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.8.7
|
||||
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:
|
||||
vite: 5.3.3
|
||||
vite: 5.3.3(@types/node@22.8.7)
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
|
@ -5,11 +5,6 @@
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<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%
|
||||
</head>
|
||||
<body class="grad-back" data-sveltekit-preload-data="hover">
|
||||
|
@ -11,9 +11,11 @@ export const ApplicationStatus = Object.freeze({
|
||||
Applyed: 4,
|
||||
Expired: 5,
|
||||
TasksToDo: 6,
|
||||
TasksToDo2: 10,
|
||||
LinkedApplication: 7,
|
||||
InterviewStep1: 8,
|
||||
InterviewStep2: 9
|
||||
InterviewStep2: 9,
|
||||
FinalInterview: 11
|
||||
});
|
||||
|
||||
export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
|
||||
@ -24,9 +26,11 @@ export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus
|
||||
4: 'send',
|
||||
5: 'hourglass-bottom',
|
||||
6: 'list-check',
|
||||
10: 'list-check',
|
||||
7: 'link-45deg',
|
||||
8: 'person',
|
||||
9: 'people'
|
||||
9: 'people',
|
||||
11: 'badge-vo-fill'
|
||||
});
|
||||
|
||||
export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
|
||||
@ -37,9 +41,11 @@ export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, s
|
||||
4: 'Applyed',
|
||||
5: 'Expired',
|
||||
6: 'Tasks To Do',
|
||||
10: 'Tasks To Do 2',
|
||||
7: 'Linked Application',
|
||||
8: 'Interview 1',
|
||||
9: 'Interview 2'
|
||||
9: 'Interview 2',
|
||||
11: 'Final Interview'
|
||||
});
|
||||
|
||||
export type View = {
|
||||
@ -73,6 +79,7 @@ export type Application = {
|
||||
payrange: string;
|
||||
status: AsEnum<typeof ApplicationStatus>;
|
||||
recruiter: string;
|
||||
agency: boolean;
|
||||
company: string;
|
||||
message: string;
|
||||
linked_application: string;
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.min.css'
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
@ -16,9 +16,18 @@
|
||||
<ApplicationsList />
|
||||
<WorkArea />
|
||||
</div>
|
||||
<PApplicationList status={ApplicationStatus.FinalInterview}>
|
||||
Interview Final
|
||||
</PApplicationList >
|
||||
<PApplicationList status={ApplicationStatus.InterviewStep2}>
|
||||
Interview II
|
||||
</PApplicationList >
|
||||
<PApplicationList status={ApplicationStatus.InterviewStep1}>
|
||||
Interview I
|
||||
</PApplicationList >
|
||||
<PApplicationList status={ApplicationStatus.TasksToDo2}>
|
||||
Tasks To do 2
|
||||
</PApplicationList >
|
||||
<PApplicationList status={ApplicationStatus.TasksToDo}>
|
||||
Tasks To do
|
||||
</PApplicationList >
|
||||
|
@ -7,18 +7,9 @@
|
||||
onMount(() => {
|
||||
applicationStore.loadApplications();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-2/12 card p-3 flex flex-col flex-shrink min-h-0">
|
||||
<h1>To Apply</h1>
|
||||
<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) => {
|
||||
let internal = $derived(
|
||||
applicationStore.applications.filter((i) => {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
@ -31,7 +22,20 @@
|
||||
}
|
||||
|
||||
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
|
||||
class="card p-2 my-2 bg-slate-100 max-w-full"
|
||||
draggable="true"
|
||||
@ -43,7 +47,10 @@
|
||||
}}
|
||||
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">
|
||||
<div class="flex-grow max-w-[90%]">
|
||||
<div class="whitespace-nowrap overflow-hidden">
|
||||
|
@ -10,11 +10,11 @@
|
||||
let filter = $state('');
|
||||
</script>
|
||||
|
||||
<div class="card p-3 rounded-lg flex flex-col">
|
||||
<div class="card p-3 rounded-lg">
|
||||
<h1 class="flex gap-2">
|
||||
Applied <input bind:value={filter} placeholder="search" class="flex-grow text-blue-500" />
|
||||
</h1>
|
||||
<div class="overflow-auto flex-grow">
|
||||
<div class="flex flex-wrap gap-4 justify-between">
|
||||
{#each applicationStore.applyed.filter((i) => {
|
||||
if (!filter) {
|
||||
return true;
|
||||
@ -30,10 +30,10 @@
|
||||
return x.match(f);
|
||||
}) as item}
|
||||
<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 () => {
|
||||
item.views = await get(`view/${item.id}`);
|
||||
item.events = await get(`events/${item.id}`);
|
||||
item.views = await get(`view/${item.id}`);
|
||||
item.events = await get(`events/${item.id}`);
|
||||
applicationStore.loadItem = item;
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
@ -50,14 +50,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
</h2>
|
||||
<a
|
||||
href={item.url}
|
||||
class="text-violet-600 overflow-hidden whitespace-nowrap block"
|
||||
>
|
||||
{item.url}
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-[40px]">
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { userStore } from '$lib/UserStore.svelte';
|
||||
</script>
|
||||
|
||||
<div class="p-7">
|
||||
<div class="p-7 pb-1">
|
||||
<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('/submit')}>
|
||||
@ -15,6 +15,9 @@
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/graphs')}>
|
||||
Graphs
|
||||
</button>
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flow')}>
|
||||
Flow
|
||||
</button>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="text-secudanry px-2">
|
||||
{userStore.user.username}
|
||||
|
@ -44,12 +44,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</h2>
|
||||
<a
|
||||
href={item.url}
|
||||
class="text-violet-600 overflow-hidden whitespace-nowrap block"
|
||||
>
|
||||
{item.url}
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { userStore } from '$lib/UserStore.svelte';
|
||||
import { get } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import ApplicationsList from '../ApplicationsList.svelte';
|
||||
|
||||
let id: string | undefined | null = $state(undefined);
|
||||
|
||||
@ -23,6 +24,7 @@
|
||||
recruiter: string;
|
||||
message: string;
|
||||
company: string;
|
||||
agency: boolean,
|
||||
flairs: SimpleFlair[];
|
||||
};
|
||||
|
||||
@ -127,15 +129,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if application}
|
||||
<h2 class="text-white p-3 text-4xl">
|
||||
👋 Hello
|
||||
{#if application.recruiter}
|
||||
<span class="font-bold">{application.recruiter}</span> @
|
||||
<span class="font-bold">{application.company}</span>
|
||||
{:else if application.company}
|
||||
recruiter @ <span class="font-bold">{application.company}</span>
|
||||
{/if}
|
||||
</h2>
|
||||
{#if !application.agency}
|
||||
<h2 class="text-white p-3 text-4xl">
|
||||
👋 Hello
|
||||
{#if application.recruiter}
|
||||
<span class="font-bold">{application.recruiter}</span> @
|
||||
<span class="font-bold">{application.company}</span>
|
||||
{:else if application.company}
|
||||
recruiter @ <span class="font-bold">{application.company}</span>
|
||||
{/if}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
{#if application.message}
|
||||
<div class="p-3 bg-white w-[190mm] rounded-lg">
|
||||
|
513
site/src/routes/flow/+page.svelte
Normal file
513
site/src/routes/flow/+page.svelte
Normal 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>
|
33
site/src/routes/flow/IconPicker.svelte
Normal file
33
site/src/routes/flow/IconPicker.svelte
Normal 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>
|
78
site/src/routes/flow/Link.svelte
Normal file
78
site/src/routes/flow/Link.svelte
Normal 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>
|
162
site/src/routes/flow/NodeRect.svelte
Normal file
162
site/src/routes/flow/NodeRect.svelte
Normal 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();
|
||||
});
|
||||
}}
|
||||
/>
|
41
site/src/routes/flow/Rect.svelte
Normal file
41
site/src/routes/flow/Rect.svelte
Normal 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>
|
5
site/src/routes/flow/generate-list-json.ts
Normal file
5
site/src/routes/flow/generate-list-json.ts
Normal 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))
|
1
site/src/routes/flow/icons-list.json
Normal file
1
site/src/routes/flow/icons-list.json
Normal file
File diff suppressed because one or more lines are too long
60
site/src/routes/flow/types.ts
Normal file
60
site/src/routes/flow/types.ts
Normal 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;
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
let showLinked = $state(false);
|
||||
let showToApply = $state(false);
|
||||
|
||||
let totalPercetange = $state(true);
|
||||
|
||||
// Handle the graph creation
|
||||
$effect(() => {
|
||||
if (!chartDiv || applications.length == 0) return;
|
||||
@ -46,6 +48,8 @@
|
||||
Linkedin: 0
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
|
||||
applications.forEach((a) => {
|
||||
let source: NodeType;
|
||||
|
||||
@ -65,6 +69,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
if (a.url.includes('linkedin')) {
|
||||
source = 'Linkedin';
|
||||
sourceData['Linkedin'] += 1;
|
||||
@ -106,6 +112,12 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (history.includes(`${ApplicationStatus.TasksToDo2}`)) {
|
||||
source = addGraph(source, ApplicationStatus.TasksToDo2);
|
||||
if (a.status == ApplicationStatus.TasksToDo2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (history.includes(`${ApplicationStatus.InterviewStep1}`)) {
|
||||
source = addGraph(source, ApplicationStatus.InterviewStep1);
|
||||
if (a.status == ApplicationStatus.InterviewStep1) {
|
||||
@ -118,6 +130,12 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (history.includes(`${ApplicationStatus.FinalInterview}`)) {
|
||||
source = addGraph(source, ApplicationStatus.FinalInterview);
|
||||
if (a.status == ApplicationStatus.FinalInterview) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
addGraph(source, ApplicationStatus.ApplyedButSaidNo);
|
||||
});
|
||||
|
||||
@ -154,7 +172,9 @@
|
||||
originalValue: node,
|
||||
id: name,
|
||||
index: i,
|
||||
percentage: Math.trunc((value / applications.length) * 100)
|
||||
percentage: Math.trunc(
|
||||
(value / (totalPercetange ? applications.length : count)) * 100
|
||||
)
|
||||
};
|
||||
return base;
|
||||
});
|
||||
@ -201,9 +221,9 @@
|
||||
// let color = d3.schemeSpectral[nodes.length];
|
||||
// let color = d3.interpolateTurbo(nodes.length);
|
||||
|
||||
function getColor(index: number) {
|
||||
return d3.interpolateRainbow(index/nodes.length);
|
||||
}
|
||||
function getColor(index: number) {
|
||||
return d3.interpolateRainbow(index / nodes.length);
|
||||
}
|
||||
|
||||
// add in the links
|
||||
var link = svg
|
||||
@ -221,10 +241,12 @@
|
||||
.attr('class', 'link')
|
||||
.attr('d', path)
|
||||
.style('stroke', function (d) {
|
||||
return d3.rgb(
|
||||
getColor(d.source.index)
|
||||
// color[d.source.index]
|
||||
).toString();
|
||||
return d3
|
||||
.rgb(
|
||||
getColor(d.source.index)
|
||||
// color[d.source.index]
|
||||
)
|
||||
.toString();
|
||||
})
|
||||
.style('stroke-width', function (d) {
|
||||
return Math.max(1, d.dy);
|
||||
@ -264,13 +286,16 @@
|
||||
.attr('width', sankey.getNodeWidth())
|
||||
.style('fill', function (d) {
|
||||
return getColor(d.index);
|
||||
//color[d.index];
|
||||
//color[d.index];
|
||||
})
|
||||
.style('stroke', (d) => {
|
||||
return d3.rgb(
|
||||
getColor(d.index)
|
||||
//color[d.index]
|
||||
).darker(2).toString();
|
||||
return d3
|
||||
.rgb(
|
||||
getColor(d.index)
|
||||
//color[d.index]
|
||||
)
|
||||
.darker(2)
|
||||
.toString();
|
||||
})
|
||||
.append('title')
|
||||
.text(function (d) {
|
||||
@ -329,6 +354,10 @@
|
||||
<label for="showToApply">Show To Apply</label>
|
||||
<input id="showToApply" type="checkbox" bind:checked={showToApply} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="totalPercetange">Total percentage</label>
|
||||
<input id="totalPercetange" type="checkbox" bind:checked={totalPercetange} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-col p-5">
|
||||
|
28
site/src/routes/work-area/CompanyField.svelte
Normal file
28
site/src/routes/work-area/CompanyField.svelte
Normal 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>
|
198
site/src/routes/work-area/DropingZone.svelte
Normal file
198
site/src/routes/work-area/DropingZone.svelte
Normal 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}
|
@ -27,8 +27,11 @@
|
||||
async function submit(item: Application) {
|
||||
try {
|
||||
application.linked_application = item.id;
|
||||
application.status = ApplicationStatus.LinkedApplication;
|
||||
await put('application/update', application);
|
||||
await put('application/status', {
|
||||
id: application.id,
|
||||
status: ApplicationStatus.LinkedApplication
|
||||
});
|
||||
dialog.close();
|
||||
onreload(item);
|
||||
} catch (e) {
|
||||
@ -70,9 +73,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
</h2>
|
||||
<span class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full">
|
||||
{item.url}
|
||||
</span>
|
||||
<span>
|
||||
{ApplicationStatusMaping[item.status]}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -36,18 +36,9 @@
|
||||
document.removeEventListener('keydown', docKey);
|
||||
};
|
||||
});
|
||||
</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>
|
||||
{applications.length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
||||
<!-- TODO loading screen -->
|
||||
{#each applications.filter((i) => {
|
||||
let internal = $derived(
|
||||
applications.filter((i) => {
|
||||
if (application && i.id == application.id) {
|
||||
return false;
|
||||
}
|
||||
@ -71,7 +62,20 @@
|
||||
}
|
||||
|
||||
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">
|
||||
<button
|
||||
class="text-left max-w-full"
|
||||
@ -90,14 +94,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{ApplicationStatusMaping[item.status]}
|
||||
</div>
|
||||
</h2>
|
||||
<span
|
||||
class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full"
|
||||
>
|
||||
{item.url}
|
||||
<span>
|
||||
{ApplicationStatusMaping[item.status]}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
ApplicationStatusMaping
|
||||
} from '$lib/ApplicationsStore.svelte';
|
||||
|
||||
let { application }: { application: Application } = $props();
|
||||
let { application, showAll }: { application: Application, showAll: boolean } = $props();
|
||||
|
||||
let events: (ApplicationEvent & { timeDiff: string })[] = $state([]);
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
if (event.event_type === EventType.Creation) {
|
||||
status = ApplicationStatus.ToApply;
|
||||
}
|
||||
if (event.event_type !== EventType.StatusUpdate) {
|
||||
if (event.event_type !== EventType.StatusUpdate || showAll) {
|
||||
if (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"
|
||||
>
|
||||
{#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}
|
||||
<span
|
||||
title="Views {new Date(event.time).toLocaleString()}"
|
||||
title={`Viewed @\n ${new Date(event.time).toLocaleString()}`}
|
||||
class="bi bi-eye"
|
||||
></span>
|
||||
{:else}
|
||||
|
@ -3,21 +3,21 @@
|
||||
ApplicationStatus,
|
||||
ApplicationStatusMaping,
|
||||
applicationStore,
|
||||
type Application,
|
||||
type AsEnum
|
||||
type Application
|
||||
} 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 ExtractTextDialog from './ExtractTextDialog.svelte';
|
||||
import Flair from '../flair/Flair.svelte';
|
||||
import NewUrlDialog from './NewUrlDialog.svelte';
|
||||
import DropZone from './DropZone.svelte';
|
||||
import { userStore } from '$lib/UserStore.svelte';
|
||||
import LinkApplication from './LinkApplication.svelte';
|
||||
import SearchApplication from './SearchApplication.svelte';
|
||||
import NewApplication from './NewApplication.svelte';
|
||||
import Timeline from './Timeline.svelte';
|
||||
import DropingZone from './DropingZone.svelte';
|
||||
import CompanyField from './CompanyField.svelte';
|
||||
|
||||
let activeItem: Application | undefined = $state();
|
||||
|
||||
@ -30,6 +30,8 @@
|
||||
|
||||
let showExtraData = $state(false);
|
||||
|
||||
let drag = $state(true);
|
||||
|
||||
async function activate(item?: Application) {
|
||||
if (!item) {
|
||||
return;
|
||||
@ -94,6 +96,9 @@
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Make the CV open on a new page
|
||||
//
|
||||
function docKey(e: KeyboardEvent) {
|
||||
if (activeItem && e.ctrlKey && e.code === 'KeyO') {
|
||||
openCV(activeItem.id);
|
||||
@ -108,6 +113,9 @@
|
||||
document.removeEventListener('keydown', docKey);
|
||||
};
|
||||
});
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
async function loadActive() {
|
||||
try {
|
||||
@ -175,46 +183,6 @@
|
||||
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() {
|
||||
try {
|
||||
await put('application/update', activeItem);
|
||||
@ -232,14 +200,6 @@
|
||||
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>
|
||||
|
||||
<div class="flex flex-col w-full gap-2 min-w-0 relative" role="none">
|
||||
@ -260,7 +220,9 @@
|
||||
<div class="w-full">
|
||||
{#if activeItem.status != 1}
|
||||
<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>
|
||||
{/if}
|
||||
{#if showExtraData}
|
||||
@ -278,15 +240,7 @@
|
||||
</fieldset>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<fieldset class="grow">
|
||||
<label class="flabel" for="title">Company</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="title"
|
||||
bind:value={activeItem.company}
|
||||
onchange={save}
|
||||
/>
|
||||
</fieldset>
|
||||
<CompanyField bind:company={activeItem.company} {save} />
|
||||
<fieldset class="grow">
|
||||
<label class="flabel" for="title">Recruiter</label>
|
||||
<input
|
||||
@ -296,6 +250,16 @@
|
||||
onchange={save}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label class="flabel" for="title">Agency</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="title"
|
||||
type="checkbox"
|
||||
bind:checked={activeItem.agency}
|
||||
onchange={save}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset>
|
||||
<label class="flabel" for="title">Title</label>
|
||||
@ -316,7 +280,7 @@
|
||||
/>
|
||||
</fieldset>
|
||||
{#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="finput bg-white w-full break-keep">
|
||||
{activeItem.url}
|
||||
@ -324,7 +288,7 @@
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if activeItem.unique_url}
|
||||
<fieldset draggable="false">
|
||||
<fieldset>
|
||||
<div class="flabel">Unique Url</div>
|
||||
<div class="finput bg-white">
|
||||
{activeItem.unique_url}
|
||||
@ -332,7 +296,7 @@
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if activeItem.linked_application}
|
||||
<fieldset draggable="false">
|
||||
<fieldset>
|
||||
<div class="flabel">Linked Application</div>
|
||||
<div class="finput bg-white">
|
||||
{activeItem.linked_application}
|
||||
@ -340,7 +304,7 @@
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if activeItem.application_time}
|
||||
<fieldset draggable="false">
|
||||
<fieldset>
|
||||
<div class="flabel">Application Time</div>
|
||||
<div class="finput bg-white">
|
||||
{activeItem.application_time}
|
||||
@ -372,7 +336,6 @@
|
||||
<fieldset>
|
||||
<label class="flabel" for="extra">Message</label>
|
||||
<textarea
|
||||
draggable={false}
|
||||
class="finput"
|
||||
id="extra"
|
||||
bind:value={activeItem.message}
|
||||
@ -442,131 +405,9 @@
|
||||
🔬
|
||||
</button>
|
||||
</div>
|
||||
<Timeline application={activeItem} />
|
||||
<Timeline application={activeItem} showAll={showExtraData} />
|
||||
</div>
|
||||
{#if applicationStore.dragging}
|
||||
<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}
|
||||
<DropingZone bind:activeItem />
|
||||
{:else}
|
||||
<div
|
||||
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 ?? ''}
|
||||
bind:dialog={changeUrl}
|
||||
onreload={async (item) => {
|
||||
item.events = await get(`events/${item.id}`);
|
||||
activeItem = item;
|
||||
}}
|
||||
item.events = await get(`events/${item.id}`);
|
||||
activeItem = item;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@ -615,7 +456,7 @@
|
||||
<SearchApplication
|
||||
application={activeItem}
|
||||
onreload={async (item) => {
|
||||
item.events = await get(`events/${item.id}`);
|
||||
item.events = await get(`events/${item.id}`);
|
||||
activeItem = item;
|
||||
}}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user