diff --git a/packages/backend/drizzle/0025_sharp_quicksilver.sql b/packages/backend/drizzle/0025_sharp_quicksilver.sql new file mode 100644 index 0000000..1ef0f8a --- /dev/null +++ b/packages/backend/drizzle/0025_sharp_quicksilver.sql @@ -0,0 +1 @@ +ALTER TABLE "User" ALTER COLUMN "iconPreference" SET DEFAULT 'pixel'; \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0025_snapshot.json b/packages/backend/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..98a6a78 --- /dev/null +++ b/packages/backend/drizzle/meta/0025_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json index 8c5e050..acb30b1 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1769295524021, "tag": "0024_military_stryfe", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1769549697892, + "tag": "0025_sharp_quicksilver", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/frontend/src/components/account.tsx b/packages/frontend/src/components/account.tsx index c8b97db..5ebe4f1 100644 --- a/packages/frontend/src/components/account.tsx +++ b/packages/frontend/src/components/account.tsx @@ -24,7 +24,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [avatarURL, setAvatarUrl] = useState(null); - const [iconPreference, setIconPreference] = useState("lucide"); + const [iconPreference, setIconPreference] = useState("pixel"); const [error, setError] = useState(""); const [submitAttempted, setSubmitAttempted] = useState(false); @@ -34,7 +34,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { setName(currentUser.name); setUsername(currentUser.username); setAvatarUrl(currentUser.avatarURL || null); - setIconPreference((currentUser.iconPreference as IconStyle) ?? "lucide"); + setIconPreference((currentUser.iconPreference as IconStyle) ?? "pixel"); setPassword(""); setError(""); @@ -136,18 +136,18 @@ function Account({ trigger }: { trigger?: ReactNode }) { - -
- - Lucide -
-
Pixel
+ +
+ + Lucide +
+
diff --git a/packages/frontend/src/components/issue-form.tsx b/packages/frontend/src/components/issue-form.tsx index d325ec7..f438892 100644 --- a/packages/frontend/src/components/issue-form.tsx +++ b/packages/frontend/src/components/issue-form.tsx @@ -1,6 +1,6 @@ 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 { MultiAssigneeSelect } from "@/components/multi-assignee-select"; import { useAuthenticatedSession } from "@/components/session-provider"; @@ -57,6 +57,10 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { const [assigneeIds, setAssigneeIds] = useState(["unassigned"]); const [status, setStatus] = useState(defaultStatus); const [type, setType] = useState(defaultType); + useEffect(() => { + if (!status && defaultStatus) setStatus(defaultStatus); + if (!type && defaultType) setType(defaultType); + }, [defaultStatus, defaultType, status, type]); const [submitAttempted, setSubmitAttempted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index b1d4d21..9093341 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -19,15 +19,18 @@ const DEMO_USERS = [ { 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 [searchParams] = useSearchParams(); const { setUser } = useSession(); const [loginDetailsOpen, setLoginDetailsOpen] = useState(false); - const [showWarning, setShowWarning] = useState(() => { - return localStorage.getItem("hide-under-construction") !== "true"; - }); const [mode, setMode] = useState<"login" | "register">("login"); @@ -143,7 +146,7 @@ export default function LogInForm() { <> {/* under construction warning */} {showWarning && ( -
+
Login Details - + Demo Login Credentials
{DEMO_USERS.map((user) => ( @@ -208,7 +211,7 @@ export default function LogInForm() {
diff --git a/packages/frontend/src/components/login-modal.tsx b/packages/frontend/src/components/login-modal.tsx new file mode 100644 index 0000000..89b5177 --- /dev/null +++ b/packages/frontend/src/components/login-modal.tsx @@ -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 ( + + + Log In or Register + + + + ); +} diff --git a/packages/frontend/src/components/selection-provider.tsx b/packages/frontend/src/components/selection-provider.tsx index f66d9b4..ff514f0 100644 --- a/packages/frontend/src/components/selection-provider.tsx +++ b/packages/frontend/src/components/selection-provider.tsx @@ -1,6 +1,7 @@ import type { IssueResponse, OrganisationResponse, ProjectResponse } from "@sprint/shared"; import type { ReactNode } from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useLocation } from "react-router-dom"; type SelectionContextValue = { selectedOrganisationId: number | null; @@ -69,6 +70,8 @@ const updateUrlParams = (updates: { }; export function SelectionProvider({ children }: { children: ReactNode }) { + const location = useLocation(); + const initialParams = useMemo(() => { const params = new URLSearchParams(window.location.search); const orgSlug = params.get("o")?.trim().toLowerCase() ?? ""; @@ -153,24 +156,25 @@ export function SelectionProvider({ children }: { children: ReactNode }) { useEffect(() => { 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: { orgSlug?: string | null; projectKey?: string | null; issueNumber?: number | null; } = {}; - if (!params.get("o")) { + if (allowParams && !params.get("o")) { const storedOrgSlug = readStoredString("selectedOrganisationSlug"); if (storedOrgSlug) updates.orgSlug = storedOrgSlug; } - if (!params.get("p")) { + if (allowParams && !params.get("p")) { const storedProjectKey = readStoredString("selectedProjectKey"); if (storedProjectKey) updates.projectKey = storedProjectKey; } - if (allowIssue && !params.get("i")) { + if (allowParams && !params.get("i")) { const storedIssueNumber = readStoredId("selectedIssueNumber"); if (storedIssueNumber != null) updates.issueNumber = storedIssueNumber; } @@ -178,7 +182,7 @@ export function SelectionProvider({ children }: { children: ReactNode }) { if (Object.keys(updates).length > 0) { updateUrlParams(updates); } - }, []); + }, [location.pathname]); const value = useMemo( () => ({ diff --git a/packages/frontend/src/components/session-provider.tsx b/packages/frontend/src/components/session-provider.tsx index 6321f74..4b0966a 100644 --- a/packages/frontend/src/components/session-provider.tsx +++ b/packages/frontend/src/components/session-provider.tsx @@ -1,7 +1,8 @@ import type { UserRecord } from "@sprint/shared"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; -import { Navigate, useLocation } from "react-router-dom"; + import Loading from "@/components/loading"; +import { LoginModal } from "@/components/login-modal"; import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils"; interface SessionContextValue { @@ -74,15 +75,22 @@ export function SessionProvider({ children }: { children: React.ReactNode }) { export function RequireAuth({ children }: { children: React.ReactNode }) { 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) { return ; } if (!user) { - const next = encodeURIComponent(location.pathname + location.search); - return ; + return ; } return <>{children}; diff --git a/packages/frontend/src/components/top-bar.tsx b/packages/frontend/src/components/top-bar.tsx index a7bed9c..fabc10e 100644 --- a/packages/frontend/src/components/top-bar.tsx +++ b/packages/frontend/src/components/top-bar.tsx @@ -48,13 +48,13 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
} /> diff --git a/packages/frontend/src/components/ui/icon.tsx b/packages/frontend/src/components/ui/icon.tsx index ff3529e..304ea47 100644 --- a/packages/frontend/src/components/ui/icon.tsx +++ b/packages/frontend/src/components/ui/icon.tsx @@ -208,7 +208,7 @@ const icons = { export type IconName = keyof typeof icons; 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 default function Icon({ @@ -227,7 +227,7 @@ export default function Icon({ const resolvedStyle = (iconStyle ?? session?.user?.iconPreference ?? localStorage.getItem("iconPreference") ?? - "lucide") as IconStyle; + "pixel") as IconStyle; const IconComponent = icons[icon]?.[resolvedStyle]; if (localStorage.getItem("iconPreference") !== resolvedStyle) diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index f214187..9e9b102 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -11,7 +11,6 @@ import { Toaster } from "@/components/ui/sonner"; import Font from "@/pages/Font"; import Issues from "@/pages/Issues"; import Landing from "@/pages/Landing"; -import Login from "@/pages/Login"; import NotFound from "@/pages/NotFound"; import Test from "@/pages/Test"; import Timeline from "@/pages/Timeline"; @@ -21,13 +20,12 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - + + {/* public routes */} } /> } /> - } /> {/* authed routes */} } /> - - - - + + + + diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx index 0ea0533..0770c04 100644 --- a/packages/frontend/src/pages/Landing.tsx +++ b/packages/frontend/src/pages/Landing.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Link } from "react-router-dom"; +import { LoginModal } from "@/components/login-modal"; import { useSession } from "@/components/session-provider"; import ThemeToggle from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; @@ -24,7 +25,6 @@ const pricingTiers = [ "Email support", ], cta: "Get started free", - ctaLink: "/login", highlighted: false, }, { @@ -45,7 +45,6 @@ const pricingTiers = [ "Priority email support", ], cta: "Try pro free for 14 days", - ctaLink: "/login", highlighted: true, }, ]; @@ -88,6 +87,7 @@ const faqs = [ export default function Landing() { const { user, isLoading } = useSession(); const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly"); + const [loginModalOpen, setLoginModalOpen] = useState(false); return (
@@ -129,8 +129,8 @@ export default function Landing() { Open app ) : ( - )}
@@ -162,8 +162,8 @@ export default function Landing() { ) : ( <> -
))} @@ -455,8 +455,8 @@ export default function Landing() { Open app ) : ( - )}
@@ -467,6 +467,8 @@ export default function Landing() {
+ +