mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
Merge pull request #4 from hex248/development
Login/register modal instead of /login
This commit is contained in:
1
packages/backend/drizzle/0025_sharp_quicksilver.sql
Normal file
1
packages/backend/drizzle/0025_sharp_quicksilver.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "User" ALTER COLUMN "iconPreference" SET DEFAULT 'pixel';
|
||||||
930
packages/backend/drizzle/meta/0025_snapshot.json
Normal file
930
packages/backend/drizzle/meta/0025_snapshot.json
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
{
|
||||||
|
"id": "c4f3042a-375d-4d17-b836-3d7816b26519",
|
||||||
|
"prevId": "95c0fe99-9c32-4baf-a635-3be4c92b90de",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.Issue": {
|
||||||
|
"name": "Issue",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "Issue_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectId": {
|
||||||
|
"name": "projectId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"name": "number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'Task'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "varchar(24)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'TO DO'"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(2048)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"creatorId": {
|
||||||
|
"name": "creatorId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sprintId": {
|
||||||
|
"name": "sprintId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"unique_project_issue_number": {
|
||||||
|
"name": "unique_project_issue_number",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "projectId",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "number",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Issue_projectId_Project_id_fk": {
|
||||||
|
"name": "Issue_projectId_Project_id_fk",
|
||||||
|
"tableFrom": "Issue",
|
||||||
|
"tableTo": "Project",
|
||||||
|
"columnsFrom": [
|
||||||
|
"projectId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Issue_creatorId_User_id_fk": {
|
||||||
|
"name": "Issue_creatorId_User_id_fk",
|
||||||
|
"tableFrom": "Issue",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": [
|
||||||
|
"creatorId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Issue_sprintId_Sprint_id_fk": {
|
||||||
|
"name": "Issue_sprintId_Sprint_id_fk",
|
||||||
|
"tableFrom": "Issue",
|
||||||
|
"tableTo": "Sprint",
|
||||||
|
"columnsFrom": [
|
||||||
|
"sprintId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.IssueAssignee": {
|
||||||
|
"name": "IssueAssignee",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "IssueAssignee_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issueId": {
|
||||||
|
"name": "issueId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"assignedAt": {
|
||||||
|
"name": "assignedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"unique_issue_user": {
|
||||||
|
"name": "unique_issue_user",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "issueId",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "userId",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"IssueAssignee_issueId_Issue_id_fk": {
|
||||||
|
"name": "IssueAssignee_issueId_Issue_id_fk",
|
||||||
|
"tableFrom": "IssueAssignee",
|
||||||
|
"tableTo": "Issue",
|
||||||
|
"columnsFrom": [
|
||||||
|
"issueId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"IssueAssignee_userId_User_id_fk": {
|
||||||
|
"name": "IssueAssignee_userId_User_id_fk",
|
||||||
|
"tableFrom": "IssueAssignee",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.IssueComment": {
|
||||||
|
"name": "IssueComment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "IssueComment_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issueId": {
|
||||||
|
"name": "issueId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"name": "body",
|
||||||
|
"type": "varchar(2048)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"IssueComment_issueId_Issue_id_fk": {
|
||||||
|
"name": "IssueComment_issueId_Issue_id_fk",
|
||||||
|
"tableFrom": "IssueComment",
|
||||||
|
"tableTo": "Issue",
|
||||||
|
"columnsFrom": [
|
||||||
|
"issueId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"IssueComment_userId_User_id_fk": {
|
||||||
|
"name": "IssueComment_userId_User_id_fk",
|
||||||
|
"tableFrom": "IssueComment",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.Organisation": {
|
||||||
|
"name": "Organisation",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "Organisation_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"iconURL": {
|
||||||
|
"name": "iconURL",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"name": "statuses",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"#a1a1a1\",\"MERGED\":\"#a1a1a1\"}'::json"
|
||||||
|
},
|
||||||
|
"issueTypes": {
|
||||||
|
"name": "issueTypes",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{\"Task\":{\"icon\":\"checkBox\",\"color\":\"#e4bd47\"},\"Bug\":{\"icon\":\"bug\",\"color\":\"#ef4444\"}}'::json"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"name": "features",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{\"userAvatars\":true,\"issueTypes\":true,\"issueStatus\":true,\"issueDescriptions\":true,\"issueTimeTracking\":true,\"issueAssignees\":true,\"issueAssigneesShownInTable\":true,\"issueCreator\":true,\"issueComments\":true,\"sprints\":true}'::json"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"Organisation_slug_unique": {
|
||||||
|
"name": "Organisation_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.OrganisationMember": {
|
||||||
|
"name": "OrganisationMember",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "OrganisationMember_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"organisationId": {
|
||||||
|
"name": "organisationId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"OrganisationMember_organisationId_Organisation_id_fk": {
|
||||||
|
"name": "OrganisationMember_organisationId_Organisation_id_fk",
|
||||||
|
"tableFrom": "OrganisationMember",
|
||||||
|
"tableTo": "Organisation",
|
||||||
|
"columnsFrom": [
|
||||||
|
"organisationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"OrganisationMember_userId_User_id_fk": {
|
||||||
|
"name": "OrganisationMember_userId_User_id_fk",
|
||||||
|
"tableFrom": "OrganisationMember",
|
||||||
|
"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": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "Project_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"organisationId": {
|
||||||
|
"name": "organisationId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"creatorId": {
|
||||||
|
"name": "creatorId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Project_organisationId_Organisation_id_fk": {
|
||||||
|
"name": "Project_organisationId_Organisation_id_fk",
|
||||||
|
"tableFrom": "Project",
|
||||||
|
"tableTo": "Organisation",
|
||||||
|
"columnsFrom": [
|
||||||
|
"organisationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Project_creatorId_User_id_fk": {
|
||||||
|
"name": "Project_creatorId_User_id_fk",
|
||||||
|
"tableFrom": "Project",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": [
|
||||||
|
"creatorId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.Session": {
|
||||||
|
"name": "Session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "Session_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"csrfToken": {
|
||||||
|
"name": "csrfToken",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.Sprint": {
|
||||||
|
"name": "Sprint",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "Sprint_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectId": {
|
||||||
|
"name": "projectId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'#a1a1a1'"
|
||||||
|
},
|
||||||
|
"startDate": {
|
||||||
|
"name": "startDate",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"endDate": {
|
||||||
|
"name": "endDate",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Sprint_projectId_Project_id_fk": {
|
||||||
|
"name": "Sprint_projectId_Project_id_fk",
|
||||||
|
"tableFrom": "Sprint",
|
||||||
|
"tableTo": "Project",
|
||||||
|
"columnsFrom": [
|
||||||
|
"projectId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.TimedSession": {
|
||||||
|
"name": "TimedSession",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "TimedSession_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"issueId": {
|
||||||
|
"name": "issueId",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"timestamps": {
|
||||||
|
"name": "timestamps",
|
||||||
|
"type": "timestamp[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"endedAt": {
|
||||||
|
"name": "endedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"TimedSession_userId_User_id_fk": {
|
||||||
|
"name": "TimedSession_userId_User_id_fk",
|
||||||
|
"tableFrom": "TimedSession",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"TimedSession_issueId_Issue_id_fk": {
|
||||||
|
"name": "TimedSession_issueId_Issue_id_fk",
|
||||||
|
"tableFrom": "TimedSession",
|
||||||
|
"tableTo": "Issue",
|
||||||
|
"columnsFrom": [
|
||||||
|
"issueId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.User": {
|
||||||
|
"name": "User",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "User_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"passwordHash": {
|
||||||
|
"name": "passwordHash",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatarURL": {
|
||||||
|
"name": "avatarURL",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"iconPreference": {
|
||||||
|
"name": "iconPreference",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'pixel'"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"User_username_unique": {
|
||||||
|
"name": "User_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -176,6 +176,13 @@
|
|||||||
"when": 1769295524021,
|
"when": 1769295524021,
|
||||||
"tag": "0024_military_stryfe",
|
"tag": "0024_military_stryfe",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769549697892,
|
||||||
|
"tag": "0025_sharp_quicksilver",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ function Account({ trigger }: { trigger?: ReactNode }) {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
||||||
const [iconPreference, setIconPreference] = useState<IconStyle>("lucide");
|
const [iconPreference, setIconPreference] = useState<IconStyle>("pixel");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ function Account({ trigger }: { trigger?: ReactNode }) {
|
|||||||
setName(currentUser.name);
|
setName(currentUser.name);
|
||||||
setUsername(currentUser.username);
|
setUsername(currentUser.username);
|
||||||
setAvatarUrl(currentUser.avatarURL || null);
|
setAvatarUrl(currentUser.avatarURL || null);
|
||||||
setIconPreference((currentUser.iconPreference as IconStyle) ?? "lucide");
|
setIconPreference((currentUser.iconPreference as IconStyle) ?? "pixel");
|
||||||
|
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setError("");
|
setError("");
|
||||||
@@ -136,18 +136,18 @@ function Account({ trigger }: { trigger?: ReactNode }) {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent position="popper" side="bottom" align="start">
|
<SelectContent position="popper" side="bottom" align="start">
|
||||||
<SelectItem value="lucide">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon icon="sun" iconStyle="lucide" size={16} />
|
|
||||||
Lucide
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="pixel">
|
<SelectItem value="pixel">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon icon="sun" iconStyle="pixel" size={16} />
|
<Icon icon="sun" iconStyle="pixel" size={16} />
|
||||||
Pixel
|
Pixel
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value="lucide">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon icon="sun" iconStyle="lucide" size={16} />
|
||||||
|
Lucide
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="phosphor">
|
<SelectItem value="phosphor">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon icon="sun" iconStyle="phosphor" size={16} />
|
<Icon icon="sun" iconStyle="phosphor" size={16} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH } from "@sprint/shared";
|
import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH } from "@sprint/shared";
|
||||||
|
|
||||||
import { type FormEvent, useMemo, useState } from "react";
|
import { type FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { MultiAssigneeSelect } from "@/components/multi-assignee-select";
|
import { MultiAssigneeSelect } from "@/components/multi-assignee-select";
|
||||||
import { useAuthenticatedSession } from "@/components/session-provider";
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
@@ -57,6 +57,10 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
|
|||||||
const [assigneeIds, setAssigneeIds] = useState<string[]>(["unassigned"]);
|
const [assigneeIds, setAssigneeIds] = useState<string[]>(["unassigned"]);
|
||||||
const [status, setStatus] = useState<string>(defaultStatus);
|
const [status, setStatus] = useState<string>(defaultStatus);
|
||||||
const [type, setType] = useState<string>(defaultType);
|
const [type, setType] = useState<string>(defaultType);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!status && defaultStatus) setStatus(defaultStatus);
|
||||||
|
if (!type && defaultType) setType(defaultType);
|
||||||
|
}, [defaultStatus, defaultType, status, type]);
|
||||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -19,15 +19,18 @@ const DEMO_USERS = [
|
|||||||
{ name: "User 2", username: "u2", password: "a" },
|
{ name: "User 2", username: "u2", password: "a" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LogInForm() {
|
export default function LogInForm({
|
||||||
|
showWarning,
|
||||||
|
setShowWarning,
|
||||||
|
}: {
|
||||||
|
showWarning: boolean;
|
||||||
|
setShowWarning: (value: boolean) => void;
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { setUser } = useSession();
|
const { setUser } = useSession();
|
||||||
|
|
||||||
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
||||||
const [showWarning, setShowWarning] = useState(() => {
|
|
||||||
return localStorage.getItem("hide-under-construction") !== "true";
|
|
||||||
});
|
|
||||||
|
|
||||||
const [mode, setMode] = useState<"login" | "register">("login");
|
const [mode, setMode] = useState<"login" | "register">("login");
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ export default function LogInForm() {
|
|||||||
<>
|
<>
|
||||||
{/* under construction warning */}
|
{/* under construction warning */}
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<div className="relative flex flex-col border p-4 items-center border-border/50 bg-border/10 gap-2 max-w-lg">
|
<div className="relative flex flex-col items-center gap-2 max-w-lg">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="md"
|
size="md"
|
||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
@@ -168,7 +171,7 @@ export default function LogInForm() {
|
|||||||
<DialogTrigger className="text-primary hover:text-personality cursor-pointer mt-2">
|
<DialogTrigger className="text-primary hover:text-personality cursor-pointer mt-2">
|
||||||
Login Details
|
Login Details
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-xs" showCloseButton={false}>
|
<DialogContent showCloseButton={false}>
|
||||||
<DialogTitle className="sr-only">Demo Login Credentials</DialogTitle>
|
<DialogTitle className="sr-only">Demo Login Credentials</DialogTitle>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{DEMO_USERS.map((user) => (
|
{DEMO_USERS.map((user) => (
|
||||||
@@ -208,7 +211,7 @@ export default function LogInForm() {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col gap-2 items-center border p-6 pb-4",
|
"relative flex flex-col gap-2 items-center p-4 pb-2",
|
||||||
error !== "" && "border-destructive",
|
error !== "" && "border-destructive",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
55
packages/frontend/src/components/login-modal.tsx
Normal file
55
packages/frontend/src/components/login-modal.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import LogInForm from "@/components/login-form";
|
||||||
|
import { useSession } from "@/components/session-provider";
|
||||||
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface LoginModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
dismissible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginModal({ open, onOpenChange, onSuccess, dismissible = true }: LoginModalProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { user, isLoading } = useSession();
|
||||||
|
const [hasRedirected, setHasRedirected] = useState(false);
|
||||||
|
const [showWarning, setShowWarning] = useState(() => {
|
||||||
|
return localStorage.getItem("hide-under-construction") !== "true";
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && !isLoading && user && !hasRedirected) {
|
||||||
|
setHasRedirected(true);
|
||||||
|
const next = searchParams.get("next") || "/issues";
|
||||||
|
navigate(next, { replace: true });
|
||||||
|
onSuccess?.();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}, [open, user, isLoading, navigate, searchParams, onSuccess, onOpenChange, hasRedirected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setHasRedirected(false);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
if (!dismissible && !newOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpenChange(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent showCloseButton={false} className={cn("p-0 w-xs py-8", showWarning && "w-md pt-4")}>
|
||||||
|
<DialogTitle className="sr-only">Log In or Register</DialogTitle>
|
||||||
|
<LogInForm showWarning={showWarning} setShowWarning={setShowWarning} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IssueResponse, OrganisationResponse, ProjectResponse } from "@sprint/shared";
|
import type { IssueResponse, OrganisationResponse, ProjectResponse } from "@sprint/shared";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
type SelectionContextValue = {
|
type SelectionContextValue = {
|
||||||
selectedOrganisationId: number | null;
|
selectedOrganisationId: number | null;
|
||||||
@@ -69,6 +70,8 @@ const updateUrlParams = (updates: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function SelectionProvider({ children }: { children: ReactNode }) {
|
export function SelectionProvider({ children }: { children: ReactNode }) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const initialParams = useMemo(() => {
|
const initialParams = useMemo(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const orgSlug = params.get("o")?.trim().toLowerCase() ?? "";
|
const orgSlug = params.get("o")?.trim().toLowerCase() ?? "";
|
||||||
@@ -153,24 +156,25 @@ export function SelectionProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const allowIssue = window.location.pathname.startsWith("/issues");
|
const pathname = location.pathname;
|
||||||
|
const allowParams = pathname.startsWith("/issues") || pathname.startsWith("/timeline");
|
||||||
const updates: {
|
const updates: {
|
||||||
orgSlug?: string | null;
|
orgSlug?: string | null;
|
||||||
projectKey?: string | null;
|
projectKey?: string | null;
|
||||||
issueNumber?: number | null;
|
issueNumber?: number | null;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (!params.get("o")) {
|
if (allowParams && !params.get("o")) {
|
||||||
const storedOrgSlug = readStoredString("selectedOrganisationSlug");
|
const storedOrgSlug = readStoredString("selectedOrganisationSlug");
|
||||||
if (storedOrgSlug) updates.orgSlug = storedOrgSlug;
|
if (storedOrgSlug) updates.orgSlug = storedOrgSlug;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.get("p")) {
|
if (allowParams && !params.get("p")) {
|
||||||
const storedProjectKey = readStoredString("selectedProjectKey");
|
const storedProjectKey = readStoredString("selectedProjectKey");
|
||||||
if (storedProjectKey) updates.projectKey = storedProjectKey;
|
if (storedProjectKey) updates.projectKey = storedProjectKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowIssue && !params.get("i")) {
|
if (allowParams && !params.get("i")) {
|
||||||
const storedIssueNumber = readStoredId("selectedIssueNumber");
|
const storedIssueNumber = readStoredId("selectedIssueNumber");
|
||||||
if (storedIssueNumber != null) updates.issueNumber = storedIssueNumber;
|
if (storedIssueNumber != null) updates.issueNumber = storedIssueNumber;
|
||||||
}
|
}
|
||||||
@@ -178,7 +182,7 @@ export function SelectionProvider({ children }: { children: ReactNode }) {
|
|||||||
if (Object.keys(updates).length > 0) {
|
if (Object.keys(updates).length > 0) {
|
||||||
updateUrlParams(updates);
|
updateUrlParams(updates);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const value = useMemo<SelectionContextValue>(
|
const value = useMemo<SelectionContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { UserRecord } from "@sprint/shared";
|
import type { UserRecord } from "@sprint/shared";
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
|
import { LoginModal } from "@/components/login-modal";
|
||||||
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
interface SessionContextValue {
|
interface SessionContextValue {
|
||||||
@@ -74,15 +75,22 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const { user, isLoading } = useSession();
|
const { user, isLoading } = useSession();
|
||||||
const location = useLocation();
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !user) {
|
||||||
|
setLoginModalOpen(true);
|
||||||
|
} else if (user) {
|
||||||
|
setLoginModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [user, isLoading]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading message={"Checking authentication"} />;
|
return <Loading message={"Checking authentication"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const next = encodeURIComponent(location.pathname + location.search);
|
return <LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} dismissible={false} />;
|
||||||
return <Navigate to={`/login?next=${next}`} replace />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
|
|||||||
<div className={`flex gap-${BREATHING_ROOM} items-center`}>
|
<div className={`flex gap-${BREATHING_ROOM} items-center`}>
|
||||||
<OrganisationSelect
|
<OrganisationSelect
|
||||||
noDecoration
|
noDecoration
|
||||||
triggerClassName="px-1 rounded-full hover:bg-transparent dark:hover:bg-transparent"
|
triggerClassName="w-8 h-8 ml-1 mr-1 rounded-full hover:bg-transparent dark:hover:bg-transparent"
|
||||||
trigger={
|
trigger={
|
||||||
<OrgIcon
|
<OrgIcon
|
||||||
name={selectedOrganisation?.Organisation.name ?? ""}
|
name={selectedOrganisation?.Organisation.name ?? ""}
|
||||||
slug={selectedOrganisation?.Organisation.slug ?? ""}
|
slug={selectedOrganisation?.Organisation.slug ?? ""}
|
||||||
iconURL={selectedOrganisation?.Organisation.iconURL || undefined}
|
iconURL={selectedOrganisation?.Organisation.iconURL || undefined}
|
||||||
size={7}
|
size={8}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ const icons = {
|
|||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
export const iconNames = Object.keys(icons) as IconName[];
|
export const iconNames = Object.keys(icons) as IconName[];
|
||||||
export const iconStyles = ["lucide", "pixel", "phosphor"] as const;
|
export const iconStyles = ["pixel", "lucide", "phosphor"] as const;
|
||||||
export type { IconStyle };
|
export type { IconStyle };
|
||||||
|
|
||||||
export default function Icon({
|
export default function Icon({
|
||||||
@@ -227,7 +227,7 @@ export default function Icon({
|
|||||||
const resolvedStyle = (iconStyle ??
|
const resolvedStyle = (iconStyle ??
|
||||||
session?.user?.iconPreference ??
|
session?.user?.iconPreference ??
|
||||||
localStorage.getItem("iconPreference") ??
|
localStorage.getItem("iconPreference") ??
|
||||||
"lucide") as IconStyle;
|
"pixel") as IconStyle;
|
||||||
const IconComponent = icons[icon]?.[resolvedStyle];
|
const IconComponent = icons[icon]?.[resolvedStyle];
|
||||||
|
|
||||||
if (localStorage.getItem("iconPreference") !== resolvedStyle)
|
if (localStorage.getItem("iconPreference") !== resolvedStyle)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Toaster } from "@/components/ui/sonner";
|
|||||||
import Font from "@/pages/Font";
|
import Font from "@/pages/Font";
|
||||||
import Issues from "@/pages/Issues";
|
import Issues from "@/pages/Issues";
|
||||||
import Landing from "@/pages/Landing";
|
import Landing from "@/pages/Landing";
|
||||||
import Login from "@/pages/Login";
|
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
import Test from "@/pages/Test";
|
import Test from "@/pages/Test";
|
||||||
import Timeline from "@/pages/Timeline";
|
import Timeline from "@/pages/Timeline";
|
||||||
@@ -21,13 +20,12 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<SelectionProvider>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<SelectionProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* public routes */}
|
{/* public routes */}
|
||||||
<Route path="/" element={<Landing />} />
|
<Route path="/" element={<Landing />} />
|
||||||
<Route path="/font" element={<Font />} />
|
<Route path="/font" element={<Font />} />
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
{/* authed routes */}
|
{/* authed routes */}
|
||||||
<Route
|
<Route
|
||||||
@@ -57,10 +55,10 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</SelectionProvider>
|
||||||
<ActiveTimersOverlay />
|
</BrowserRouter>
|
||||||
<Toaster visibleToasts={1} duration={2000} />
|
<ActiveTimersOverlay />
|
||||||
</SelectionProvider>
|
<Toaster visibleToasts={1} duration={2000} />
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { LoginModal } from "@/components/login-modal";
|
||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import ThemeToggle from "@/components/theme-toggle";
|
import ThemeToggle from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -24,7 +25,6 @@ const pricingTiers = [
|
|||||||
"Email support",
|
"Email support",
|
||||||
],
|
],
|
||||||
cta: "Get started free",
|
cta: "Get started free",
|
||||||
ctaLink: "/login",
|
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,6 @@ const pricingTiers = [
|
|||||||
"Priority email support",
|
"Priority email support",
|
||||||
],
|
],
|
||||||
cta: "Try pro free for 14 days",
|
cta: "Try pro free for 14 days",
|
||||||
ctaLink: "/login",
|
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -88,6 +87,7 @@ const faqs = [
|
|||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
const { user, isLoading } = useSession();
|
const { user, isLoading } = useSession();
|
||||||
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
|
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
|
||||||
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col" id="top">
|
<div className="min-h-screen flex flex-col" id="top">
|
||||||
@@ -129,8 +129,8 @@ export default function Landing() {
|
|||||||
<Link to="/issues">Open app</Link>
|
<Link to="/issues">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button variant="outline" size="sm" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Sign in</Link>
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -162,8 +162,8 @@ export default function Landing() {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button asChild size="lg" className="text-lg px-8 py-6">
|
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Start free trial</Link>
|
Start free trial
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
|
||||||
<a href="#pricing">See pricing</a>
|
<a href="#pricing">See pricing</a>
|
||||||
@@ -364,14 +364,14 @@ export default function Landing() {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
|
||||||
variant={tier.highlighted ? "default" : "outline"}
|
variant={tier.highlighted ? "default" : "outline"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-700 py-6",
|
"font-700 py-6",
|
||||||
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
|
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setLoginModalOpen(true)}
|
||||||
>
|
>
|
||||||
<Link to={tier.ctaLink}>{tier.cta}</Link>
|
{tier.cta}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -455,8 +455,8 @@ export default function Landing() {
|
|||||||
<Link to="/issues">Open app</Link>
|
<Link to="/issues">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild size="lg" className="text-lg px-8 py-6">
|
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Start your free trial</Link>
|
Start your free trial
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -467,6 +467,8 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
|
||||||
|
|
||||||
<footer className="flex justify-center gap-2 items-center py-1 border-t">
|
<footer className="flex justify-center gap-2 items-center py-1 border-t">
|
||||||
<span className="font-300 text-lg text-muted-foreground">
|
<span className="font-300 text-lg text-muted-foreground">
|
||||||
Built by{" "}
|
Built by{" "}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
||||||
import Loading from "@/components/loading";
|
|
||||||
import LogInForm from "@/components/login-form";
|
|
||||||
import { useSession } from "@/components/session-provider";
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const { user, isLoading } = useSession();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && user) {
|
|
||||||
const next = searchParams.get("next") || "/issues";
|
|
||||||
navigate(next, { replace: true });
|
|
||||||
}
|
|
||||||
}, [user, isLoading, navigate, searchParams]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading message="Checking authentication" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return <Loading message="Redirecting" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
|
||||||
<LogInForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ export const DEFAULT_FEATURES: Record<string, boolean> = {
|
|||||||
sprints: true,
|
sprints: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const iconStyles = ["lucide", "pixel", "phosphor"] as const;
|
export const iconStyles = ["pixel", "lucide", "phosphor"] as const;
|
||||||
export type IconStyle = (typeof iconStyles)[number];
|
export type IconStyle = (typeof iconStyles)[number];
|
||||||
|
|
||||||
export const User = pgTable("User", {
|
export const User = pgTable("User", {
|
||||||
@@ -58,7 +58,7 @@ export const User = pgTable("User", {
|
|||||||
username: varchar({ length: USER_USERNAME_MAX_LENGTH }).notNull().unique(),
|
username: varchar({ length: USER_USERNAME_MAX_LENGTH }).notNull().unique(),
|
||||||
passwordHash: varchar({ length: 255 }).notNull(),
|
passwordHash: varchar({ length: 255 }).notNull(),
|
||||||
avatarURL: varchar({ length: 512 }),
|
avatarURL: varchar({ length: 512 }),
|
||||||
iconPreference: varchar({ length: 10 }).notNull().default("lucide").$type<IconStyle>(),
|
iconPreference: varchar({ length: 10 }).notNull().default("pixel").$type<IconStyle>(),
|
||||||
createdAt: timestamp({ withTimezone: false }).defaultNow(),
|
createdAt: timestamp({ withTimezone: false }).defaultNow(),
|
||||||
updatedAt: timestamp({ withTimezone: false }).defaultNow(),
|
updatedAt: timestamp({ withTimezone: false }).defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
3
todo.md
3
todo.md
@@ -1,8 +1,6 @@
|
|||||||
# HIGH PRIORITY
|
# HIGH PRIORITY
|
||||||
|
|
||||||
- BUGS:
|
- BUGS:
|
||||||
- org slug and project code should only be added to url on issues/timeline pages. it happens on any page right now
|
|
||||||
- on the first attempt since page load, pressing the create issue button the default type and status are not loaded
|
|
||||||
- FEATURES:
|
- FEATURES:
|
||||||
- make login/register into a modal that appears atop the landing page
|
- make login/register into a modal that appears atop the landing page
|
||||||
- user preferences
|
- user preferences
|
||||||
@@ -30,3 +28,4 @@
|
|||||||
- figure out if it's possible to remove the "lib/server/..." helpers altogether, and have some sort of dynamic route maker in the shared package
|
- figure out if it's possible to remove the "lib/server/..." helpers altogether, and have some sort of dynamic route maker in the shared package
|
||||||
- request logging
|
- request logging
|
||||||
- explore payment providers (stripe is the only one i know)
|
- explore payment providers (stripe is the only one i know)
|
||||||
|
- trial system
|
||||||
Reference in New Issue
Block a user