diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7e8bab1..fc2fe38 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,8 @@ "image": "mcr.microsoft.com/devcontainers/typescript-node", // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/itsmechlark/features/postgresql:1": {} + "ghcr.io/itsmechlark/features/postgresql:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ diff --git a/drizzle/0020_ambitious_sugar_man.sql b/drizzle/0020_ambitious_sugar_man.sql new file mode 100644 index 0000000..0a14154 --- /dev/null +++ b/drizzle/0020_ambitious_sugar_man.sql @@ -0,0 +1,6 @@ +ALTER TABLE "market_item_order" ALTER COLUMN "status" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "market_item_order" ALTER COLUMN "status" SET DEFAULT 'awaiting_approval'::text;--> statement-breakpoint +DROP TYPE "public"."market_order_status";--> statement-breakpoint +CREATE TYPE "public"."market_order_status" AS ENUM('awaiting_approval', 'fulfilled', 'denied', 'refunded');--> statement-breakpoint +ALTER TABLE "market_item_order" ALTER COLUMN "status" SET DEFAULT 'awaiting_approval'::"public"."market_order_status";--> statement-breakpoint +ALTER TABLE "market_item_order" ALTER COLUMN "status" SET DATA TYPE "public"."market_order_status" USING "status"::"public"."market_order_status"; \ No newline at end of file diff --git a/drizzle/0021_perfect_meteorite.sql b/drizzle/0021_perfect_meteorite.sql new file mode 100644 index 0000000..5e677d1 --- /dev/null +++ b/drizzle/0021_perfect_meteorite.sql @@ -0,0 +1,2 @@ +ALTER TABLE "t2_review" ADD COLUMN "shopScoreMultiplier" real DEFAULT 25 NOT NULL;--> statement-breakpoint +ALTER TABLE "t2_review" DROP COLUMN "currencyMultiplier"; \ No newline at end of file diff --git a/drizzle/meta/0020_snapshot.json b/drizzle/meta/0020_snapshot.json new file mode 100644 index 0000000..540f554 --- /dev/null +++ b/drizzle/meta/0020_snapshot.json @@ -0,0 +1,1100 @@ +{ + "id": "0cf94b17-601a-4346-8d32-065a5c0c3c69", + "prevId": "2406dbb1-cdda-4c04-b6f1-363172327705", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.devlog": { + "name": "devlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timeSpent": { + "name": "timeSpent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "devlog_userId_user_id_fk": { + "name": "devlog_userId_user_id_fk", + "tableFrom": "devlog", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "devlog_projectId_project_id_fk": { + "name": "devlog_projectId_project_id_fk", + "tableFrom": "devlog", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.legion_review": { + "name": "legion_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filamentUsed": { + "name": "filamentUsed", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "legion_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "legion_review_userId_user_id_fk": { + "name": "legion_review_userId_user_id_fk", + "tableFrom": "legion_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "legion_review_projectId_project_id_fk": { + "name": "legion_review_projectId_project_id_fk", + "tableFrom": "legion_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_item": { + "name": "market_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "minRequiredShopScore": { + "name": "minRequiredShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "minShopScore": { + "name": "minShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "maxShopScore": { + "name": "maxShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "maxPrice": { + "name": "maxPrice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minPrice": { + "name": "minPrice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "isPublic": { + "name": "isPublic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "market_item_createdBy_user_id_fk": { + "name": "market_item_createdBy_user_id_fk", + "tableFrom": "market_item", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_item_order": { + "name": "market_item_order", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "addressId": { + "name": "addressId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bricksPaid": { + "name": "bricksPaid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "market_order_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'awaiting_approval'" + }, + "userNotes": { + "name": "userNotes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "market_item_order_userId_user_id_fk": { + "name": "market_item_order_userId_user_id_fk", + "tableFrom": "market_item_order", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editorFileType": { + "name": "editorFileType", + "type": "editor_file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "editorUrl": { + "name": "editorUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadedFileUrl": { + "name": "uploadedFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "modelFile": { + "name": "modelFile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'building'" + }, + "printedBy": { + "name": "printedBy", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "submittedToAirtable": { + "name": "submittedToAirtable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_userId_user_id_fk": { + "name": "project_userId_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_printedBy_user_id_fk": { + "name": "project_printedBy_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": [ + "printedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ship": { + "name": "ship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "editorFileType": { + "name": "editorFileType", + "type": "editor_file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "editorUrl": { + "name": "editorUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadedFileUrl": { + "name": "uploadedFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "modelFile": { + "name": "modelFile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ship_userId_user_id_fk": { + "name": "ship_userId_user_id_fk", + "tableFrom": "ship", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ship_projectId_project_id_fk": { + "name": "ship_projectId_project_id_fk", + "tableFrom": "ship", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.t1_review": { + "name": "t1_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "t1_review_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "t1_review_userId_user_id_fk": { + "name": "t1_review_userId_user_id_fk", + "tableFrom": "t1_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "t1_review_projectId_project_id_fk": { + "name": "t1_review_projectId_project_id_fk", + "tableFrom": "t1_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.t2_review": { + "name": "t2_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currencyMultiplier": { + "name": "currencyMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "t2_review_userId_user_id_fk": { + "name": "t2_review_userId_user_id_fk", + "tableFrom": "t2_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "t2_review_projectId_project_id_fk": { + "name": "t2_review_projectId_project_id_fk", + "tableFrom": "t2_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "idvId": { + "name": "idvId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idvToken": { + "name": "idvToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profilePicture": { + "name": "profilePicture", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hackatimeTrust": { + "name": "hackatimeTrust", + "type": "hackatime_trust", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trust": { + "name": "trust", + "type": "trust", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'blue'" + }, + "clay": { + "name": "clay", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "brick": { + "name": "brick", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "shopScore": { + "name": "shopScore", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hasBasePrinter": { + "name": "hasBasePrinter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasT1Review": { + "name": "hasT1Review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasT2Review": { + "name": "hasT2Review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasAdmin": { + "name": "hasAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isPrinter": { + "name": "isPrinter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastLoginAt": { + "name": "lastLoginAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_idvId_unique": { + "name": "user_idvId_unique", + "nullsNotDistinct": false, + "columns": [ + "idvId" + ] + }, + "user_slackId_unique": { + "name": "user_slackId_unique", + "nullsNotDistinct": false, + "columns": [ + "slackId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.editor_file_type": { + "name": "editor_file_type", + "schema": "public", + "values": [ + "url", + "upload" + ] + }, + "public.hackatime_trust": { + "name": "hackatime_trust", + "schema": "public", + "values": [ + "green", + "blue", + "yellow", + "red" + ] + }, + "public.legion_action": { + "name": "legion_action", + "schema": "public", + "values": [ + "mark_for_printing", + "unmark_for_printing", + "print", + "add_comment", + "reject", + "already_printed" + ] + }, + "public.market_order_status": { + "name": "market_order_status", + "schema": "public", + "values": [ + "awaiting_approval", + "fulfilled", + "denied", + "refunded" + ] + }, + "public.project_audit_log_type": { + "name": "project_audit_log_type", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "building", + "submitted", + "t1_approved", + "printing", + "printed", + "t2_approved", + "finalized", + "rejected", + "rejected_locked" + ] + }, + "public.t1_review_action": { + "name": "t1_review_action", + "schema": "public", + "values": [ + "approve", + "approve_no_print", + "add_comment", + "reject", + "reject_lock" + ] + }, + "public.trust": { + "name": "trust", + "schema": "public", + "values": [ + "green", + "blue", + "yellow", + "red" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0021_snapshot.json b/drizzle/meta/0021_snapshot.json new file mode 100644 index 0000000..41ffea4 --- /dev/null +++ b/drizzle/meta/0021_snapshot.json @@ -0,0 +1,1100 @@ +{ + "id": "a04b623b-57d1-4225-ad78-e97531504ffc", + "prevId": "0cf94b17-601a-4346-8d32-065a5c0c3c69", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.devlog": { + "name": "devlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timeSpent": { + "name": "timeSpent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "devlog_userId_user_id_fk": { + "name": "devlog_userId_user_id_fk", + "tableFrom": "devlog", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "devlog_projectId_project_id_fk": { + "name": "devlog_projectId_project_id_fk", + "tableFrom": "devlog", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.legion_review": { + "name": "legion_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filamentUsed": { + "name": "filamentUsed", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "legion_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "legion_review_userId_user_id_fk": { + "name": "legion_review_userId_user_id_fk", + "tableFrom": "legion_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "legion_review_projectId_project_id_fk": { + "name": "legion_review_projectId_project_id_fk", + "tableFrom": "legion_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_item": { + "name": "market_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "minRequiredShopScore": { + "name": "minRequiredShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "minShopScore": { + "name": "minShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "maxShopScore": { + "name": "maxShopScore", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "maxPrice": { + "name": "maxPrice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minPrice": { + "name": "minPrice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "isPublic": { + "name": "isPublic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "market_item_createdBy_user_id_fk": { + "name": "market_item_createdBy_user_id_fk", + "tableFrom": "market_item", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_item_order": { + "name": "market_item_order", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "addressId": { + "name": "addressId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bricksPaid": { + "name": "bricksPaid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "market_order_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'awaiting_approval'" + }, + "userNotes": { + "name": "userNotes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "market_item_order_userId_user_id_fk": { + "name": "market_item_order_userId_user_id_fk", + "tableFrom": "market_item_order", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editorFileType": { + "name": "editorFileType", + "type": "editor_file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "editorUrl": { + "name": "editorUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadedFileUrl": { + "name": "uploadedFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "modelFile": { + "name": "modelFile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'building'" + }, + "printedBy": { + "name": "printedBy", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "submittedToAirtable": { + "name": "submittedToAirtable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_userId_user_id_fk": { + "name": "project_userId_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_printedBy_user_id_fk": { + "name": "project_printedBy_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": [ + "printedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ship": { + "name": "ship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "editorFileType": { + "name": "editorFileType", + "type": "editor_file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "editorUrl": { + "name": "editorUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadedFileUrl": { + "name": "uploadedFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "modelFile": { + "name": "modelFile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ship_userId_user_id_fk": { + "name": "ship_userId_user_id_fk", + "tableFrom": "ship", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ship_projectId_project_id_fk": { + "name": "ship_projectId_project_id_fk", + "tableFrom": "ship", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.t1_review": { + "name": "t1_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "t1_review_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "t1_review_userId_user_id_fk": { + "name": "t1_review_userId_user_id_fk", + "tableFrom": "t1_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "t1_review_projectId_project_id_fk": { + "name": "t1_review_projectId_project_id_fk", + "tableFrom": "t1_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.t2_review": { + "name": "t2_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shopScoreMultiplier": { + "name": "shopScoreMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "t2_review_userId_user_id_fk": { + "name": "t2_review_userId_user_id_fk", + "tableFrom": "t2_review", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "t2_review_projectId_project_id_fk": { + "name": "t2_review_projectId_project_id_fk", + "tableFrom": "t2_review", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "idvId": { + "name": "idvId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idvToken": { + "name": "idvToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profilePicture": { + "name": "profilePicture", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hackatimeTrust": { + "name": "hackatimeTrust", + "type": "hackatime_trust", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trust": { + "name": "trust", + "type": "trust", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'blue'" + }, + "clay": { + "name": "clay", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "brick": { + "name": "brick", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "shopScore": { + "name": "shopScore", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hasBasePrinter": { + "name": "hasBasePrinter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasT1Review": { + "name": "hasT1Review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasT2Review": { + "name": "hasT2Review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasAdmin": { + "name": "hasAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isPrinter": { + "name": "isPrinter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastLoginAt": { + "name": "lastLoginAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_idvId_unique": { + "name": "user_idvId_unique", + "nullsNotDistinct": false, + "columns": [ + "idvId" + ] + }, + "user_slackId_unique": { + "name": "user_slackId_unique", + "nullsNotDistinct": false, + "columns": [ + "slackId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.editor_file_type": { + "name": "editor_file_type", + "schema": "public", + "values": [ + "url", + "upload" + ] + }, + "public.hackatime_trust": { + "name": "hackatime_trust", + "schema": "public", + "values": [ + "green", + "blue", + "yellow", + "red" + ] + }, + "public.legion_action": { + "name": "legion_action", + "schema": "public", + "values": [ + "mark_for_printing", + "unmark_for_printing", + "print", + "add_comment", + "reject", + "already_printed" + ] + }, + "public.market_order_status": { + "name": "market_order_status", + "schema": "public", + "values": [ + "awaiting_approval", + "fulfilled", + "denied", + "refunded" + ] + }, + "public.project_audit_log_type": { + "name": "project_audit_log_type", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "building", + "submitted", + "t1_approved", + "printing", + "printed", + "t2_approved", + "finalized", + "rejected", + "rejected_locked" + ] + }, + "public.t1_review_action": { + "name": "t1_review_action", + "schema": "public", + "values": [ + "approve", + "approve_no_print", + "add_comment", + "reject", + "reject_lock" + ] + }, + "public.trust": { + "name": "trust", + "schema": "public", + "values": [ + "green", + "blue", + "yellow", + "red" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 36ecf62..0f8a951 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -141,6 +141,20 @@ "when": 1766613085822, "tag": "0019_wide_gamora", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1766693140982, + "tag": "0020_ambitious_sugar_man", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1767215125050, + "tag": "0021_perfect_meteorite", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/assets/sticker1.png b/src/lib/assets/sticker1.png index c8fd685..0805719 100644 Binary files a/src/lib/assets/sticker1.png and b/src/lib/assets/sticker1.png differ diff --git a/src/lib/assets/sticker2.png b/src/lib/assets/sticker2.png index 51b1df5..c03fd42 100644 Binary files a/src/lib/assets/sticker2.png and b/src/lib/assets/sticker2.png differ diff --git a/src/lib/currency.ts b/src/lib/currency.ts new file mode 100644 index 0000000..57ba7c9 --- /dev/null +++ b/src/lib/currency.ts @@ -0,0 +1,57 @@ +import { + BETA_DATE_CUTOFF, + BETA_MULTIPLIER, + BRICKS_PER_HOUR, + CLAY_PER_HOUR, + PRINT_MINUTES_PER_GRAM +} from './defs'; + +export function minutesToClay(minutes: number) { + const hours = minutes / 60; + + return hours * CLAY_PER_HOUR; +} + +export function minutesToBricks(minutes: number) { + const hours = minutes / 60; + + return hours * BRICKS_PER_HOUR; +} + +export function calculateShopScore(minutes: number, multiplier: number) { + const hours = minutes / 60; + + return hours * multiplier; +} + +export function calculateMinutes(timeSpent: number, printGrams: number) { + return timeSpent - printGrams * PRINT_MINUTES_PER_GRAM; +} + +export function calculateCurrencyPayout( + minutes: number, + hasBasePrinter: boolean, + dateCreated: Date +) { + return dateCreated.getTime() < BETA_DATE_CUTOFF.getTime() + ? { clay: null, bricks: minutesToBricks(minutes) * BETA_MULTIPLIER } + : hasBasePrinter + ? { clay: null, bricks: minutesToBricks(minutes) } + : { clay: minutesToClay(minutes), bricks: null }; +} + +export function calculatePayouts( + timeSpent: number, + printGrams: number, + shopScoreMultiplier: number, + hasBasePrinter: boolean, + dateCreated: Date +) { + const time = calculateMinutes(timeSpent, printGrams); + const currency = calculateCurrencyPayout(time, hasBasePrinter, dateCreated); + const shopScore = + calculateShopScore(timeSpent, shopScoreMultiplier) * + (dateCreated.getTime() < BETA_DATE_CUTOFF.getTime() ? BETA_MULTIPLIER : 1); + + return { ...currency, shopScore }; +} diff --git a/src/lib/defs.ts b/src/lib/defs.ts index 06a0b2b..d3b5824 100644 --- a/src/lib/defs.ts +++ b/src/lib/defs.ts @@ -2,4 +2,17 @@ export const DEVLOG_MIN_TIME = 5; export const DEVLOG_MAX_TIME = 120; export const DEVLOG_DESCRIPTION_MIN_WORDS = 20; -export const DEVLOG_DESCRIPTION_MAX_WORDS = 2000; \ No newline at end of file +export const DEVLOG_DESCRIPTION_MAX_WORDS = 2000; + +export const BASE_PRINTER_CLAY = 40; + +export const CLAY_PER_HOUR = 1.0; +export const BRICKS_PER_HOUR = 12.0; +export const BASE_SHOP_SCORE_PER_HOUR = 25.0; +export const BRICKS_PER_CLAY_CONVERTED = 10.0; + +export const PRINT_PAYOUT_CENTS_PER_GRAM = 2.0; +export const PRINT_MINUTES_PER_GRAM = 0.24; + +export const BETA_DATE_CUTOFF = new Date('2025-12-16'); +export const BETA_MULTIPLIER = 1.5; \ No newline at end of file diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index f34ab02..5791543 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -182,7 +182,7 @@ export const t2Review = pgTable('t2_review', { feedback: text(), notes: text(), - currencyMultiplier: real().notNull().default(1.0), + shopScoreMultiplier: real().notNull().default(25.0), timestamp: timestamp().notNull().defaultNow() }); @@ -230,7 +230,6 @@ export const marketItem = pgTable('market_item', { export const marketOrderStatus = pgEnum('market_order_status', [ 'awaiting_approval', - 'approved', 'fulfilled', 'denied', 'refunded' diff --git a/src/routes/dashboard/+layout.server.ts b/src/routes/dashboard/+layout.server.ts index 5d87aab..266027d 100644 --- a/src/routes/dashboard/+layout.server.ts +++ b/src/routes/dashboard/+layout.server.ts @@ -18,7 +18,8 @@ export function load({ locals }) { isPrinter: locals.user.isPrinter, hasT1Review: locals.user.hasT1Review, hasT2Review: locals.user.hasT2Review, - hasAdmin: locals.user.hasAdmin + hasAdmin: locals.user.hasAdmin, + hasBasePrinter: locals.user.hasBasePrinter }, s3PublicUrl: env.S3_PUBLIC_URL }; diff --git a/src/routes/dashboard/admin/admin/+page.svelte b/src/routes/dashboard/admin/admin/+page.svelte index 06b91fc..601dac5 100644 --- a/src/routes/dashboard/admin/admin/+page.svelte +++ b/src/routes/dashboard/admin/admin/+page.svelte @@ -1,6 +1,6 @@ @@ -10,7 +10,7 @@

Admin

-
+
diff --git a/src/routes/dashboard/admin/admin/orders/+page.server.ts b/src/routes/dashboard/admin/admin/orders/+page.server.ts new file mode 100644 index 0000000..64b2711 --- /dev/null +++ b/src/routes/dashboard/admin/admin/orders/+page.server.ts @@ -0,0 +1,120 @@ +import { db } from '$lib/server/db/index.js'; +import { project, user, devlog } from '$lib/server/db/schema.js'; +import { error } from '@sveltejs/kit'; +import { eq, and, sql, ne, inArray } from 'drizzle-orm'; +import type { Actions } from './$types'; + +export async function load({ locals }) { + if (!locals.user) { + throw error(500); + } + if (!locals.user.hasT2Review) { + throw error(403, { message: 'oi get out' }); + } + + const projects = await getProjects(['printed'], [], []); + + const allProjects = await db + .select({ + id: project.id, + name: project.name + }) + .from(project) + .where(and(eq(project.deleted, false))); + + const users = await db + .select({ + id: user.id, + name: user.name + }) + .from(user) + .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'))); // hide banned users + + return { + allProjects, + projects, + users + }; +} + +export const actions = { + default: async ({ locals, request }) => { + if (!locals.user) { + throw error(500); + } + if (!locals.user.hasT2Review) { + throw error(403, { message: 'oi get out' }); + } + + const data = await request.formData(); + const statusFilter = data.getAll('status') as (typeof project.status._.data)[]; + + const projectFilter = data.getAll('project').map((projectId) => { + const parsedInt = parseInt(projectId.toString()); + if (!parsedInt) throw error(400, { message: 'malformed project filter' }); + return parseInt(projectId.toString()); + }); + + const userFilter = data.getAll('user').map((userId) => { + const parsedInt = parseInt(userId.toString()); + if (!parsedInt) throw error(400, { message: 'malformed user filter' }); + return parseInt(userId.toString()); + }); + + const projects = await getProjects(statusFilter, projectFilter, userFilter); + + return { + projects, + fields: { + status: statusFilter, + project: projectFilter, + user: userFilter + } + }; + } +} satisfies Actions; + +async function getProjects( + statusFilter: (typeof project.status._.data)[], + projectFilter: number[], + userFilter: number[] +) { + return await db + .select({ + project: { + id: project.id, + name: project.name, + description: project.description, + url: project.url, + createdAt: project.createdAt, + status: project.status + }, + user: { + id: user.id, + name: user.name + }, + timeSpent: sql`COALESCE(SUM(${devlog.timeSpent}), 0)`, + devlogCount: sql`COALESCE(COUNT(${devlog.id}), 0)` + }) + .from(project) + .leftJoin(devlog, and(eq(project.id, devlog.projectId), eq(devlog.deleted, false))) + .leftJoin(user, eq(user.id, project.userId)) + .where( + and( + eq(project.deleted, false), + statusFilter.length > 0 ? inArray(project.status, statusFilter) : undefined, + projectFilter.length > 0 ? inArray(project.id, projectFilter) : undefined, + userFilter.length > 0 ? inArray(project.userId, userFilter) : undefined + ) + ) + .groupBy( + project.id, + project.name, + project.description, + project.url, + project.createdAt, + project.status, + user.id, + user.name + ); +} diff --git a/src/routes/dashboard/admin/admin/orders/+page.svelte b/src/routes/dashboard/admin/admin/orders/+page.svelte new file mode 100644 index 0000000..bf4957c --- /dev/null +++ b/src/routes/dashboard/admin/admin/orders/+page.svelte @@ -0,0 +1,207 @@ + + + + +
+

Market item orders

+ + coming soon + + + + + + + + + + +
diff --git a/src/routes/dashboard/admin/admin/orders/[id]/+page.server.ts b/src/routes/dashboard/admin/admin/orders/[id]/+page.server.ts new file mode 100644 index 0000000..342afae --- /dev/null +++ b/src/routes/dashboard/admin/admin/orders/[id]/+page.server.ts @@ -0,0 +1,266 @@ +import { db } from '$lib/server/db/index.js'; +import { project, user, devlog, t2Review } from '$lib/server/db/schema.js'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { eq, and, asc, sql, desc } from 'drizzle-orm'; +import type { Actions } from './$types'; +import { sendSlackDM } from '$lib/server/slack.js'; +import { airtableBase } from '$lib/server/airtable'; +import { env } from '$env/dynamic/private'; +import { decrypt } from '$lib/server/encryption'; +import { getUserData } from '$lib/server/idvUserData'; + +export async function load({ locals, params }) { + if (!locals.user) { + throw error(500); + } + if (!locals.user.hasAdmin) { + throw error(403, { message: 'oi get out' }); + } + + const id: number = parseInt(params.id); + + // const [queriedProject] = await db + // .select({ + // project: { + // id: project.id, + // name: project.name, + // description: project.description, + + // url: project.url, + // editorFileType: project.editorFileType, + // editorUrl: project.editorUrl, + // uploadedFileUrl: project.uploadedFileUrl, + // modelFile: project.modelFile, + + // submittedToAirtable: project.submittedToAirtable, + + // createdAt: project.createdAt, + // updatedAt: project.updatedAt, + // status: project.status + // }, + // user: { + // id: user.id, + // name: user.name, + // slackID: user.slackId, + // trust: user.trust, + // hackatimeTrust: user.hackatimeTrust + // }, + // timeSpent: sql`COALESCE(SUM(${devlog.timeSpent}), 0)`, + // devlogCount: sql`COALESCE(COUNT(${devlog.id}), 0)` + // }) + // .from(project) + // .leftJoin(devlog, and(eq(project.id, devlog.projectId), eq(devlog.deleted, false))) + // .leftJoin(user, eq(user.id, project.userId)) + // .where(and(eq(project.id, id), eq(project.deleted, false))) + // .groupBy( + // project.id, + // project.name, + // project.description, + // project.url, + // project.editorFileType, + // project.editorUrl, + // project.uploadedFileUrl, + // project.modelFile, + // project.submittedToAirtable, + // project.createdAt, + // project.status, + // user.id, + // user.name, + // user.slackId, + // user.trust, + // user.hackatimeTrust + // ) + // .limit(1); + + // if (!queriedProject) { + // throw error(404, { message: 'project not found' }); + // } + + // const devlogs = await db + // .select() + // .from(devlog) + // .where(and(eq(devlog.projectId, queriedProject.project.id), eq(devlog.deleted, false))) + // .orderBy(asc(devlog.createdAt)); + + // return { + // project: queriedProject, + // devlogs, + // reviews: await getReviewHistory(id) + // }; +} + +// export const actions = { +// default: async ({ locals, request, params }) => { +// if (!locals.user) { +// throw error(500); +// } +// if (!locals.user.hasT2Review) { +// throw error(403, { message: 'oi get out' }); +// } + +// const id: number = parseInt(params.id); + +// const [queriedProject] = await db +// .select({ +// project: { +// id: project.id, +// name: project.name, +// description: project.description, + +// url: project.url, +// editorFileType: project.editorFileType, +// editorUrl: project.editorUrl, +// uploadedFileUrl: project.uploadedFileUrl, +// submittedToAirtable: project.submittedToAirtable +// }, +// user: { +// id: user.id, +// name: user.name, +// slackId: user.slackId, +// idvId: user.idvId, +// idvToken: user.idvToken, +// trust: user.trust, +// hackatimeTrust: user.hackatimeTrust +// }, +// timeSpent: sql`COALESCE(SUM(${devlog.timeSpent}), 0)`, +// devlogCount: sql`COALESCE(COUNT(${devlog.id}), 0)` +// }) +// .from(project) +// .leftJoin(devlog, and(eq(project.id, devlog.projectId), eq(devlog.deleted, false))) +// .leftJoin(user, eq(user.id, project.userId)) +// .where(and(eq(project.id, id), eq(project.deleted, false))) +// .groupBy( +// project.id, +// project.name, +// project.description, +// project.url, +// project.editorFileType, +// project.editorUrl, +// project.uploadedFileUrl, +// user.id, +// user.name, +// user.slackId, +// user.idvId, +// user.idvToken, +// user.trust, +// user.hackatimeTrust +// ) +// .limit(1); + +// if (!queriedProject) { +// return error(404, { message: 'project not found' }); +// } + +// const data = await request.formData(); +// const notes = data.get('notes')?.toString(); +// const feedback = data.get('feedback')?.toString(); + +// if (notes === null || feedback === null) { +// return error(400); +// } + +// const status: typeof project.status._.data | undefined = 'finalized'; +// const statusMessage = 'finalised! :woah-dino:'; + +// if (airtableBase && !queriedProject.project.submittedToAirtable) { +// const [latestDevlog] = await db +// .select({ +// image: devlog.image +// }) +// .from(devlog) +// .where(and(eq(devlog.projectId, id), eq(devlog.deleted, false))) +// .orderBy(desc(devlog.createdAt)) +// .limit(1); + +// if (!queriedProject.user?.idvToken) { +// return fail(400, { +// message: 'IDV token revoked/expired, ask them to reauthenticate' +// }); +// } + +// const token = decrypt(queriedProject.user.idvToken); +// let userData; + +// try { +// userData = await getUserData(token); +// } catch { +// return fail(400, { +// message: 'IDV token revoked/expired, ask them to reauthenticate' +// }); +// } +// const { first_name, last_name, primary_email, birthday, addresses } = userData; + +// const address = addresses.find((address: { primary: boolean; }) => address.primary); + +// const repoUrl = +// queriedProject.project.editorFileType === 'upload' +// ? `${env.S3_PUBLIC_URL}/${queriedProject.project.uploadedFileUrl}` +// : queriedProject.project.editorFileType === 'url' +// ? queriedProject.project.editorUrl +// : ''; + +// const justificationAppend = `Project has ${queriedProject.devlogCount} ${queriedProject.devlogCount == 1 ? 'journal' : 'journals'} over ${Math.floor( +// queriedProject.timeSpent / 60 +// )}h ${queriedProject.timeSpent % 60}min, each one with a 3d model file to show progress.\nAll journals can be found here: https://construct.hackclub.com/dashboard/projects/${queriedProject.project.id}`; + +// await airtableBase('YSWS Project Submission').create({ +// 'Repository URL': repoUrl ?? '', +// 'Demo URL': queriedProject.project.url ?? '', +// Description: queriedProject.project.description ?? '', + +// 'First Name': first_name ?? '', +// 'Last Name': last_name ?? '', +// 'Email': primary_email ?? '', +// 'Birthday': birthday ?? '', +// 'Address (Line 1)': address?.line_1 ?? '', +// 'City': address?.city ?? '', +// 'State / Province': address?.state ?? '', +// 'Country': address?.country ?? '', +// 'ZIP / Postal Code': address?.postal_code ?? '', + +// 'Hours estimate': queriedProject.timeSpent / 60, +// 'Optional - Override Hours Spent': queriedProject.timeSpent / 60, +// 'Optional - Override Hours Spent Justification': notes +// ? notes + '\n' + justificationAppend +// : justificationAppend, +// Screenshot: [ +// { +// url: env.S3_PUBLIC_URL + '/' + latestDevlog.image +// } +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// ] as any, +// 'Slack Username': queriedProject.user?.name, +// idv_rec: queriedProject.user?.idvId, +// 'Identity Verified': true, +// 'Temp hold': false +// }); +// } + +// await db.insert(t2Review).values({ +// projectId: id, +// userId: locals.user.id, +// currencyMultiplier: 1.0, // TODO: implement +// notes, +// feedback +// }); + +// await db +// .update(project) +// .set({ +// status, +// submittedToAirtable: true +// }) +// .where(eq(project.id, id)); + +// if (queriedProject.user) { +// const feedbackText = feedback ? `\n\nHere's what they said:\n${feedback}` : ''; + +// await sendSlackDM( +// queriedProject.user.slackId, +// `Your project has been ${statusMessage}${feedbackText}` +// ); +// } + +// return redirect(302, '/dashboard/admin/review'); +// } +// } satisfies Actions; diff --git a/src/routes/dashboard/admin/admin/orders/[id]/+page.svelte b/src/routes/dashboard/admin/admin/orders/[id]/+page.svelte new file mode 100644 index 0000000..b7e4388 --- /dev/null +++ b/src/routes/dashboard/admin/admin/orders/[id]/+page.svelte @@ -0,0 +1,159 @@ + + + + + +
+ +
diff --git a/src/routes/dashboard/admin/ysws-review/[id]/+page.server.ts b/src/routes/dashboard/admin/ysws-review/[id]/+page.server.ts index d27142f..e1d8e01 100644 --- a/src/routes/dashboard/admin/ysws-review/[id]/+page.server.ts +++ b/src/routes/dashboard/admin/ysws-review/[id]/+page.server.ts @@ -1,5 +1,5 @@ import { db } from '$lib/server/db/index.js'; -import { project, user, devlog, t2Review } from '$lib/server/db/schema.js'; +import { project, user, devlog, t2Review, legionReview } from '$lib/server/db/schema.js'; import { error, fail, redirect } from '@sveltejs/kit'; import { eq, and, asc, sql, desc } from 'drizzle-orm'; import type { Actions } from './$types'; @@ -9,6 +9,7 @@ import { env } from '$env/dynamic/private'; import { decrypt } from '$lib/server/encryption'; import { getUserData } from '$lib/server/idvUserData'; import { getReviewHistory } from '../../getReviewHistory.server'; +import { calculatePayouts } from '$lib/currency'; export async function load({ locals, params }) { if (!locals.user) { @@ -86,12 +87,13 @@ export async function load({ locals, params }) { return { project: queriedProject, devlogs, - reviews: await getReviewHistory(id) + reviews: await getReviewHistory(id), + filamentUsed: await getLatestPrintFilament(id) }; } export const actions = { - default: async ({ locals, request, params }) => { + review: async ({ locals, request, params }) => { if (!locals.user) { throw error(500); } @@ -107,6 +109,7 @@ export const actions = { id: project.id, name: project.name, description: project.description, + createdAt: project.createdAt, url: project.url, editorFileType: project.editorFileType, @@ -121,7 +124,12 @@ export const actions = { idvId: user.idvId, idvToken: user.idvToken, trust: user.trust, - hackatimeTrust: user.hackatimeTrust + hackatimeTrust: user.hackatimeTrust, + hasBasePrinter: user.hasBasePrinter, + + clay: user.clay, + brick: user.brick, + shopScore: user.shopScore }, timeSpent: sql`COALESCE(SUM(${devlog.timeSpent}), 0)`, devlogCount: sql`COALESCE(COUNT(${devlog.id}), 0)` @@ -134,6 +142,7 @@ export const actions = { project.id, project.name, project.description, + project.createdAt, project.url, project.editorFileType, project.editorUrl, @@ -144,7 +153,11 @@ export const actions = { user.idvId, user.idvToken, user.trust, - user.hackatimeTrust + user.hackatimeTrust, + user.hasBasePrinter, + user.clay, + user.brick, + user.shopScore ) .limit(1); @@ -155,11 +168,22 @@ export const actions = { const data = await request.formData(); const notes = data.get('notes')?.toString(); const feedback = data.get('feedback')?.toString(); + const shopScoreMultiplier = data.get('shopScoreMultiplier'); if (notes === null || feedback === null) { return error(400); } + if ( + !shopScoreMultiplier || + isNaN(parseFloat(shopScoreMultiplier.toString())) || + parseFloat(shopScoreMultiplier.toString()) < 0 + ) { + return error(400, { message: 'invalid market score multiplier' }); + } + + const parsedShopScoreMultiplier = parseFloat(shopScoreMultiplier.toString()); + const status: typeof project.status._.data | undefined = 'finalized'; const statusMessage = 'finalised! :woah-dino:'; @@ -191,7 +215,7 @@ export const actions = { } const { first_name, last_name, primary_email, birthday, addresses } = userData; - const address = addresses.find((address: { primary: boolean; }) => address.primary); + const address = addresses.find((address: { primary: boolean }) => address.primary); const repoUrl = queriedProject.project.editorFileType === 'upload' @@ -211,12 +235,12 @@ export const actions = { 'First Name': first_name ?? '', 'Last Name': last_name ?? '', - 'Email': primary_email ?? '', - 'Birthday': birthday ?? '', + Email: primary_email ?? '', + Birthday: birthday ?? '', 'Address (Line 1)': address?.line_1 ?? '', - 'City': address?.city ?? '', + City: address?.city ?? '', 'State / Province': address?.state ?? '', - 'Country': address?.country ?? '', + Country: address?.country ?? '', 'ZIP / Postal Code': address?.postal_code ?? '', 'Hours estimate': queriedProject.timeSpent / 60, @@ -240,9 +264,9 @@ export const actions = { await db.insert(t2Review).values({ projectId: id, userId: locals.user.id, - currencyMultiplier: 1.0, // TODO: implement notes, - feedback + feedback, + shopScoreMultiplier: parsedShopScoreMultiplier }); await db @@ -254,6 +278,23 @@ export const actions = { .where(eq(project.id, id)); if (queriedProject.user) { + const payouts = calculatePayouts( + queriedProject.timeSpent, + await getLatestPrintFilament(id), + parsedShopScoreMultiplier, + queriedProject.user.hasBasePrinter, + queriedProject.project.createdAt + ); + + await db + .update(user) + .set({ + clay: sql`${user.clay} + ${payouts.clay ?? 0}`, + brick: sql`${user.brick} + ${payouts.bricks ?? 0}`, + shopScore: sql`${user.shopScore} + ${payouts.shopScore}` + }) + .where(eq(user.id, queriedProject.user.id)); + const feedbackText = feedback ? `\n\nHere's what they said:\n${feedback}` : ''; await sendSlackDM( @@ -263,5 +304,68 @@ export const actions = { } return redirect(302, '/dashboard/admin/review'); + }, + + override: async ({ locals, request, params }) => { + if (!locals.user) { + throw error(500); + } + if (!locals.user.hasT2Review) { + throw error(403, { message: 'oi get out' }); + } + + const id: number = parseInt(params.id); + + const data = await request.formData(); + const devlogId = data.get('devlogId'); + const timeSpent = data.get('minutes'); + + if (!devlogId || isNaN(parseInt(devlogId.toString()))) { + return error(400, { message: 'invalid devlog id' }); + } + + const parsedDevlogId = parseInt(devlogId.toString()); + + if (!timeSpent || isNaN(parseInt(timeSpent.toString())) || parseInt(timeSpent.toString()) < 0) { + return error(400, { message: 'invalid time spent' }); + } + + const parsedTimeSpent = parseInt(timeSpent.toString()); + + const [queriedDevlog] = await db + .select({ id: devlog.id }) + .from(devlog) + .where( + and(eq(devlog.deleted, false), eq(devlog.projectId, id), eq(devlog.id, parsedDevlogId)) + ) + .limit(1); + + if (!queriedDevlog) { + return error(404, { message: 'devlog not found' }); + } + + await db + .update(devlog) + .set({ + timeSpent: parsedTimeSpent + }) + .where( + and(eq(devlog.deleted, false), eq(devlog.projectId, id), eq(devlog.id, parsedDevlogId)) + ); + + return { success: true }; } } satisfies Actions; + +async function getLatestPrintFilament(id: number) { + const [{ filament }] = (await db + .select({ + filament: legionReview.filamentUsed + }) + .from(legionReview) + .where(and(eq(legionReview.projectId, id), eq(legionReview.action, 'print'))) + .orderBy(desc(legionReview.timestamp)) + .limit(1)) ?? [{ filament: 0 }]; + + return filament ?? 0; +} diff --git a/src/routes/dashboard/admin/ysws-review/[id]/+page.svelte b/src/routes/dashboard/admin/ysws-review/[id]/+page.svelte index 82a7cfd..4da5ab8 100644 --- a/src/routes/dashboard/admin/ysws-review/[id]/+page.svelte +++ b/src/routes/dashboard/admin/ysws-review/[id]/+page.svelte @@ -8,10 +8,24 @@ import Spinny3DPreview from '$lib/components/Spinny3DPreview.svelte'; import { Download } from '@lucide/svelte'; import ReviewHistory from '../../ReviewHistory.svelte'; + import { calculatePayouts, minutesToClay } from '$lib/currency'; + import { BASE_SHOP_SCORE_PER_HOUR } from '$lib/defs'; let { data, form } = $props(); let formPending = $state(false); + let overridePending = $state(false); + + let shopScoreMultiplier = $state(BASE_SHOP_SCORE_PER_HOUR); + let payouts = $derived.by(() => + calculatePayouts( + data.project.timeSpent, + data.filamentUsed, + shopScoreMultiplier, + data.user.hasBasePrinter, + data.project.project.createdAt + ) + ); @@ -41,6 +55,7 @@ .project.timeSpent % 60}min

Status: {projectStatuses[data.project.project.status]}

+

Filament spent printing: {data.filamentUsed}g

Submitted to Airtable: {data.project.project.submittedToAirtable ?? 'null (false)'}

{ formPending = true; @@ -138,6 +154,27 @@ + + +

+ Payouts: {Math.round((payouts.clay ?? 0) * 10) / 10} clay, {Math.round( + (payouts.bricks ?? 0) * 10 + ) / 10} bricks, + {Math.round(payouts.shopScore * 10) / 10} market score +

+ {#if form?.message}

{form?.message}

{/if} @@ -151,7 +188,43 @@

Journal logs

{#each data.devlogs as devlog} - +
+ +
+ { + overridePending = true; + return async ({ update }) => { + await update({ reset: false }); + overridePending = false; + }; + }} + > + + + + + + +
+
{/each}
diff --git a/src/routes/dashboard/projects/[id]/ship/+page.svelte b/src/routes/dashboard/projects/[id]/ship/+page.svelte index bc2e0ff..fefb4a1 100644 --- a/src/routes/dashboard/projects/[id]/ship/+page.svelte +++ b/src/routes/dashboard/projects/[id]/ship/+page.svelte @@ -3,6 +3,7 @@ import ChecklistItem from '$lib/components/ChecklistItem.svelte'; import Head from '$lib/components/Head.svelte'; import Project from '$lib/components/Project.svelte'; + import { calculateCurrencyPayout, calculateMinutes } from '$lib/currency'; import { MAX_UPLOAD_SIZE } from '../config'; import type { PageProps } from './$types'; import { Ship, SquarePen } from '@lucide/svelte'; @@ -17,6 +18,15 @@ let modelFile = $state(null); let hasEditorFile = $derived((editorUrl || editorUploadFile) && !(editorUrl && editorUploadFile)); + + let filamentUse = $state(50); + let payoutEstimate = $derived.by(() => + calculateCurrencyPayout( + calculateMinutes(data.project.timeSpent, filamentUse), + data.user.hasBasePrinter, + data.project.createdAt + ) + ); @@ -177,21 +187,21 @@ and meets the open definition or Orpheus will come after you with a stick. I'd recommend using CC-BY-SA!

-
- {#if data.project.timeSpent >= 120 && data.project.description != '' && data.project.url != ''} -

- Are you sure you want to ship "{data.project.name}"? - You won't be able to edit it or journal again unless it gets - rejected. -

- {/if} -
+
+
+ {#if data.project.timeSpent >= 120 && data.project.description != '' && data.project.url != ''} +

+ Are you sure you want to ship "{data.project.name}"? + You won't be able to edit it or journal again unless it gets rejected. +

+ {/if}
Cancel
+ +{#if data.project.timeSpent >= 120} +
+

Estimate payout

+
+ +

+ You'll get {payoutEstimate.clay + ? Math.round(payoutEstimate.clay * 10) / 10 + ' clay' + : Math.round((payoutEstimate.bricks ?? 0) * 10) / 10 + ' bricks'} +

+

+ This is just an estimate, not a guarantee - your journal time might be adjusted after + review. +

+
+
+{:else} +
+{/if}