frontend indentation set to 2

This commit is contained in:
Oliver Bryan
2026-01-21 17:47:04 +00:00
parent 70504b3056
commit 5a5e40659c
117 changed files with 7548 additions and 7785 deletions

View File

@@ -5,7 +5,7 @@
"enabled": true, "enabled": true,
"formatWithErrors": false, "formatWithErrors": false,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 2,
"lineWidth": 110 "lineWidth": 110
}, },
"css": { "css": {

View File

@@ -131,10 +131,7 @@ function Account({ trigger }: { trigger?: ReactNode }) {
</div> </div>
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<Label className="text-sm">Icon Style</Label> <Label className="text-sm">Icon Style</Label>
<Select <Select value={iconPreference} onValueChange={(v) => setIconPreference(v as IconStyle)}>
value={iconPreference}
onValueChange={(v) => setIconPreference(v as IconStyle)}
>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>

View File

@@ -111,10 +111,7 @@ export function AddMember({
Cancel Cancel
</Button> </Button>
</DialogClose> </DialogClose>
<Button <Button type="submit" disabled={submitting || (username.trim() === "" && submitAttempted)}>
type="submit"
disabled={submitting || (username.trim() === "" && submitAttempted)}
>
{submitting ? "Adding..." : "Add"} {submitting ? "Adding..." : "Add"}
</Button> </Button>
</div> </div>

View File

@@ -286,9 +286,7 @@ export function IssueDetails({
} catch (error) { } catch (error) {
console.error(`error deleting issue ${issueID(projectKey, issueData.Issue.number)}`, error); console.error(`error deleting issue ${issueID(projectKey, issueData.Issue.number)}`, error);
toast.error( toast.error(
`Error deleting issue ${issueID(projectKey, issueData.Issue.number)}: ${parseError( `Error deleting issue ${issueID(projectKey, issueData.Issue.number)}: ${parseError(error as Error)}`,
error as Error,
)}`,
{ {
dismissible: false, dismissible: false,
}, },
@@ -303,9 +301,7 @@ export function IssueDetails({
{showHeader && ( {showHeader && (
<div className="flex flex-row items-center justify-end border-b h-[25px]"> <div className="flex flex-row items-center justify-end border-b h-[25px]">
<span className="w-full"> <span className="w-full">
<p className="text-sm w-fit px-1 font-700"> <p className="text-sm w-fit px-1 font-700">{issueID(projectKey, issueData.Issue.number)}</p>
{issueID(projectKey, issueData.Issue.number)}
</p>
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<IconButton onClick={handleCopyLink} title={linkCopied ? "Copied" : "Copy link"}> <IconButton onClick={handleCopyLink} title={linkCopied ? "Copied" : "Copy link"}>
@@ -334,11 +330,7 @@ export function IssueDetails({
chevronClassName="hidden" chevronClassName="hidden"
isOpen={isOpen} isOpen={isOpen}
> >
<StatusTag <StatusTag status={value} colour={statuses[value]} className="hover:opacity-85" />
status={value}
colour={statuses[value]}
className="hover:opacity-85"
/>
</SelectTrigger> </SelectTrigger>
)} )}
/> />

View File

@@ -153,11 +153,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
chevronClassName="hidden" chevronClassName="hidden"
isOpen={isOpen} isOpen={isOpen}
> >
<StatusTag <StatusTag status={value} colour={statuses[value]} className="hover:opacity-85" />
status={value}
colour={statuses[value]}
className="hover:opacity-85"
/>
</SelectTrigger> </SelectTrigger>
)} )}
/> />
@@ -203,11 +199,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
{members.length > 0 && ( {members.length > 0 && (
<div className="flex items-start gap-2 mt-4"> <div className="flex items-start gap-2 mt-4">
<Label className="text-sm pt-2">Assignees</Label> <Label className="text-sm pt-2">Assignees</Label>
<MultiAssigneeSelect <MultiAssigneeSelect users={members} assigneeIds={assigneeIds} onChange={setAssigneeIds} />
users={members}
assigneeIds={assigneeIds}
onChange={setAssigneeIds}
/>
</div> </div>
)} )}
@@ -231,8 +223,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
submitting || submitting ||
((title.trim() === "" || title.trim().length > ISSUE_TITLE_MAX_LENGTH) && ((title.trim() === "" || title.trim().length > ISSUE_TITLE_MAX_LENGTH) &&
submitAttempted) || submitAttempted) ||
(description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH && (description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH && submitAttempted)
submitAttempted)
} }
> >
{submitting ? "Creating..." : "Create"} {submitting ? "Creating..." : "Create"}

View File

@@ -64,11 +64,7 @@ export function IssueTimer({ issueId, onEnd }: { issueId: number; onEnd?: (data:
<Button onClick={handleToggle}> <Button onClick={handleToggle}>
{!timerState ? "Start" : timerState.isRunning ? "Pause" : "Resume"} {!timerState ? "Start" : timerState.isRunning ? "Pause" : "Resume"}
</Button> </Button>
<Button <Button onClick={handleEnd} variant="outline" disabled={!timerState || timerState.endedAt != null}>
onClick={handleEnd}
variant="outline"
disabled={!timerState || timerState.endedAt != null}
>
End End
</Button> </Button>
</div> </div>

View File

@@ -87,10 +87,7 @@ export function IssuesTable({
className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent" className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
> >
{(columns.status == null || columns.status === true) && ( {(columns.status == null || columns.status === true) && (
<StatusTag <StatusTag status={issueData.Issue.status} colour={statuses[issueData.Issue.status]} />
status={issueData.Issue.status}
colour={statuses[issueData.Issue.status]}
/>
)} )}
<span className="truncate">{issueData.Issue.title}</span> <span className="truncate">{issueData.Issue.title}</span>
</a> </a>

View File

@@ -4,9 +4,7 @@ export default function Loading({ message, children }: { message?: string; child
return ( return (
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]"> <div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
<Spinner className="size-6" /> <Spinner className="size-6" />
{message && ( {message && <span className="text-xs px-2 py-1 border-2 border-input border-dashed">{message}</span>}
<span className="text-xs px-2 py-1 border-2 border-input border-dashed">{message}</span>
)}
{children} {children}
</div> </div>
); );

View File

@@ -158,8 +158,8 @@ export default function LogInForm() {
<Icon icon="alertTriangle" className="w-16 h-16 text-yellow-500" /> <Icon icon="alertTriangle" className="w-16 h-16 text-yellow-500" />
<div className="text-center text-sm text-muted-foreground font-500"> <div className="text-center text-sm text-muted-foreground font-500">
<p> <p>
This application is currently under construction. Your data is very likely to be This application is currently under construction. Your data is very likely to be lost at some
lost at some point. point.
</p> </p>
<p className="font-700 underline underline-offset-3 text-foreground/85 decoration-yellow-500 mt-2"> <p className="font-700 underline underline-offset-3 text-foreground/85 decoration-yellow-500 mt-2">
It is not recommended for production use. It is not recommended for production use.
@@ -191,16 +191,10 @@ export default function LogInForm() {
</div> </div>
<div className="text-sm text-muted-foreground space-y-1"> <div className="text-sm text-muted-foreground space-y-1">
<p> <p>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">Username:</span> {user.username}
Username:
</span>{" "}
{user.username}
</p> </p>
<p> <p>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">Password:</span> {user.password}
Password:
</span>{" "}
{user.password}
</p> </p>
</div> </div>
</button> </button>

View File

@@ -68,11 +68,7 @@ export default function OrgIcon({
)} )}
> >
{iconURL ? ( {iconURL ? (
<img <img src={iconURL} alt={name} className={`rounded-md object-cover w-${size || 6} h-${size || 6}`} />
src={iconURL}
alt={name}
className={`rounded-md object-cover w-${size || 6} h-${size || 6}`}
/>
) : ( ) : (
<span className={textClass}>{getInitials(name)}</span> <span className={textClass}>{getInitials(name)}</span>
)} )}

View File

@@ -92,10 +92,7 @@ export function OrganisationSelect({
<SelectGroup> <SelectGroup>
<SelectLabel>Organisations</SelectLabel> <SelectLabel>Organisations</SelectLabel>
{organisations.map((organisation) => ( {organisations.map((organisation) => (
<SelectItem <SelectItem key={organisation.Organisation.id} value={`${organisation.Organisation.id}`}>
key={organisation.Organisation.id}
value={`${organisation.Organisation.id}`}
>
<OrgIcon <OrgIcon
name={organisation.Organisation.name} name={organisation.Organisation.name}
slug={organisation.Organisation.slug} slug={organisation.Organisation.slug}

View File

@@ -186,12 +186,9 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error( toast.error(`Error ${action.slice(0, -1)}ing ${memberName} to ${newRole}: ${String(err)}`, {
`Error ${action.slice(0, -1)}ing ${memberName} to ${newRole}: ${String(err)}`,
{
dismissible: false, dismissible: false,
}, });
);
} }
}, },
}); });
@@ -217,12 +214,9 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
userId: memberUserId, userId: memberUserId,
}); });
closeConfirmDialog(); closeConfirmDialog();
toast.success( toast.success(`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`, {
`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`,
{
dismissible: false, dismissible: false,
}, });
);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error( toast.error(
@@ -259,8 +253,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
if (statusAdded) { if (statusAdded) {
toast.success( toast.success(
<> <>
Created <StatusTag status={statusAdded.name} colour={statusAdded.colour} /> status Created <StatusTag status={statusAdded.name} colour={statusAdded.colour} /> status successfully
successfully
</>, </>,
{ {
dismissible: false, dismissible: false,
@@ -279,8 +272,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
} else if (statusMoved) { } else if (statusMoved) {
toast.success( toast.success(
<> <>
Moved <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from Moved <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from position
position
{statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1} successfully {statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1} successfully
</>, </>,
{ {
@@ -304,8 +296,8 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
} else if (statusRemoved) { } else if (statusRemoved) {
toast.error( toast.error(
<> <>
Error removing <StatusTag status={statusRemoved.name} colour={statusRemoved.colour} />{" "} Error removing <StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> from{" "}
from {selectedOrganisation.Organisation.name}: {String(err)} {selectedOrganisation.Organisation.name}: {String(err)}
</>, </>,
{ {
dismissible: false, dismissible: false,
@@ -314,8 +306,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
} else if (statusMoved) { } else if (statusMoved) {
toast.error( toast.error(
<> <>
Error moving <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from Error moving <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from position
position
{statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "} {statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "}
{selectedOrganisation.Organisation.name}: {String(err)} {selectedOrganisation.Organisation.name}: {String(err)}
</>, </>,
@@ -414,18 +405,17 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
newStatus: reassignToStatus, newStatus: reassignToStatus,
}); });
const newStatuses = Object.keys(statuses).filter((item) => item !== statusToRemove); const newStatuses = Object.keys(statuses).filter((item) => item !== statusToRemove);
await updateStatuses( await updateStatuses(Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])), {
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])), name: statusToRemove,
{ name: statusToRemove, colour: statuses[statusToRemove] }, colour: statuses[statusToRemove],
); });
setStatusToRemove(null); setStatusToRemove(null);
setReassignToStatus(""); setReassignToStatus("");
} catch (error) { } catch (error) {
console.error("error replacing status:", error); console.error("error replacing status:", error);
toast.error( toast.error(
<> <>
Error removing <StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />{" "} Error removing <StatusTag status={statusToRemove} colour={statuses[statusToRemove]} /> from
from
{selectedOrganisation.Organisation.name}: {String(error)} {selectedOrganisation.Organisation.name}: {String(error)}
</>, </>,
{ {
@@ -460,9 +450,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full min-w-0"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full min-w-0">
<div className="flex flex-wrap gap-2 items-center w-full min-w-0"> <div className="flex flex-wrap gap-2 items-center w-full min-w-0">
<OrganisationSelect <OrganisationSelect
contentClass={ contentClass={"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"}
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
}
/> />
<TabsList> <TabsList>
<TabsTrigger value="info">Info</TabsTrigger> <TabsTrigger value="info">Info</TabsTrigger>
@@ -477,9 +465,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-col gap-0 mb-2"> <div className="flex flex-col gap-0 mb-2">
{" "} {" "}
<h2 className="text-xl font-600 break-all"> <h2 className="text-xl font-600 break-all">{selectedOrganisation.Organisation.name}</h2>
{selectedOrganisation.Organisation.name}
</h2>
<p className="text-sm text-muted-foreground break-all"> <p className="text-sm text-muted-foreground break-all">
Slug: {selectedOrganisation.Organisation.slug} Slug: {selectedOrganisation.Organisation.slug}
</p> </p>
@@ -498,22 +484,14 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{selectedOrganisation.Organisation.description ? ( {selectedOrganisation.Organisation.description ? (
<p className="text-sm break-words"> <p className="text-sm break-words">{selectedOrganisation.Organisation.description}</p>
{selectedOrganisation.Organisation.description}
</p>
) : ( ) : (
<p className="text-sm text-muted-foreground break-words"> <p className="text-sm text-muted-foreground break-words">No description</p>
No description
</p>
)} )}
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button <Button variant="outline" size="sm" onClick={() => setEditOrgOpen(true)}>
variant="outline"
size="sm"
onClick={() => setEditOrgOpen(true)}
>
<Icon icon="edit" className="size-4" /> <Icon icon="edit" className="size-4" />
Edit Edit
</Button> </Button>
@@ -531,9 +509,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
variant: "destructive", variant: "destructive",
onConfirm: async () => { onConfirm: async () => {
try { try {
await deleteOrganisation.mutateAsync( await deleteOrganisation.mutateAsync(selectedOrganisation.Organisation.id);
selectedOrganisation.Organisation.id,
);
closeConfirmDialog(); closeConfirmDialog();
toast.success( toast.success(
`Deleted organisation "${selectedOrganisation.Organisation.name}"`, `Deleted organisation "${selectedOrganisation.Organisation.name}"`,
@@ -593,38 +569,20 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
handleRoleChange( handleRoleChange(
member.User.id, member.User.id,
member.User.name, member.User.name,
member.OrganisationMember member.OrganisationMember.role,
.role,
) )
} }
variant={ variant={member.OrganisationMember.role === "admin" ? "yellow" : "green"}
member.OrganisationMember.role ===
"admin"
? "yellow"
: "green"
}
> >
{member.OrganisationMember.role === {member.OrganisationMember.role === "admin" ? (
"admin" ? ( <Icon icon="chevronDown" className="size-5" />
<Icon
icon="chevronDown"
className="size-5"
/>
) : ( ) : (
<Icon <Icon icon="chevronUp" className="size-5" />
icon="chevronUp"
className="size-5"
/>
)} )}
</IconButton> </IconButton>
<IconButton <IconButton
variant="destructive" variant="destructive"
onClick={() => onClick={() => handleRemoveMember(member.User.id, member.User.name)}
handleRemoveMember(
member.User.id,
member.User.name,
)
}
> >
<Icon icon="x" className="size-5" /> <Icon icon="x" className="size-5" />
</IconButton> </IconButton>
@@ -680,11 +638,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button <Button variant="outline" size="sm" onClick={() => setEditProjectOpen(true)}>
variant="outline"
size="sm"
onClick={() => setEditProjectOpen(true)}
>
<Icon icon="edit" className="size-4" /> <Icon icon="edit" className="size-4" />
Edit Edit
</Button> </Button>
@@ -702,10 +656,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
variant: "destructive", variant: "destructive",
onConfirm: async () => { onConfirm: async () => {
try { try {
await deleteProject.mutateAsync( await deleteProject.mutateAsync(selectedProject.Project.id);
selectedProject
.Project.id,
);
closeConfirmDialog(); closeConfirmDialog();
toast.success( toast.success(
`Deleted project "${selectedProject.Project.name}"`, `Deleted project "${selectedProject.Project.name}"`,
@@ -719,10 +670,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
}); });
}} }}
> >
<Icon <Icon icon="trash" className="size-4" />
icon="trash"
className="size-4"
/>
Delete Delete
</Button> </Button>
)} )}
@@ -730,9 +678,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
)} )}
</> </>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Select a project to view details.</p>
Select a project to view details.
</p>
)} )}
</div> </div>
<div className="flex flex-col gap-2 min-w-0 flex-1"> <div className="flex flex-col gap-2 min-w-0 flex-1">
@@ -746,17 +692,13 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<div <div
key={sprintItem.id} key={sprintItem.id}
className={`flex items-center justify-between p-2 border ${ className={`flex items-center justify-between p-2 border ${
isCurrent isCurrent ? "border-emerald-500/60 bg-emerald-500/10" : ""
? "border-emerald-500/60 bg-emerald-500/10"
: ""
}`} }`}
> >
<SmallSprintDisplay sprint={sprintItem} /> <SmallSprintDisplay sprint={sprintItem} />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{dateRange && ( {dateRange && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">{dateRange}</span>
{dateRange}
</span>
)} )}
{isAdmin && ( {isAdmin && (
<DropdownMenu> <DropdownMenu>
@@ -766,10 +708,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
noStyle noStyle
className="hover:opacity-80 cursor-pointer" className="hover:opacity-80 cursor-pointer"
> >
<Icon <Icon icon="ellipsisVertical" className="size-4 text-foreground" />
icon="ellipsisVertical"
className="size-4 text-foreground"
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
@@ -778,19 +717,12 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
> >
<DropdownMenuItem <DropdownMenuItem
onSelect={() => { onSelect={() => {
setEditingSprint( setEditingSprint(sprintItem);
sprintItem, setEditSprintOpen(true);
);
setEditSprintOpen(
true,
);
}} }}
className="hover:bg-primary-foreground" className="hover:bg-primary-foreground"
> >
<Icon <Icon icon="edit" className="size-4 text-muted-foreground" />
icon="edit"
className="size-4 text-muted-foreground"
/>
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@@ -800,37 +732,24 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
open: true, open: true,
title: "Delete Sprint", title: "Delete Sprint",
message: `Are you sure you want to delete "${sprintItem.name}"? Issues assigned to this sprint will become unassigned.`, message: `Are you sure you want to delete "${sprintItem.name}"? Issues assigned to this sprint will become unassigned.`,
confirmText: confirmText: "Delete",
"Delete", processingText: "Deleting...",
processingText: variant: "destructive",
"Deleting...", onConfirm: async () => {
variant:
"destructive",
onConfirm:
async () => {
try { try {
await deleteSprint.mutateAsync( await deleteSprint.mutateAsync(sprintItem.id);
sprintItem.id,
);
closeConfirmDialog(); closeConfirmDialog();
toast.success( toast.success(`Deleted sprint "${sprintItem.name}"`);
`Deleted sprint "${sprintItem.name}"`,
);
await invalidateSprints(); await invalidateSprints();
} catch (error) { } catch (error) {
console.error( console.error(error);
error,
);
} }
}, },
}); });
}} }}
className="hover:bg-destructive/10" className="hover:bg-destructive/10"
> >
<Icon <Icon icon="trash" className="size-4" />
icon="trash"
className="size-4"
/>
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -845,11 +764,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
projectId={selectedProject?.Project.id} projectId={selectedProject?.Project.id}
trigger={ trigger={
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
Create sprint{" "} Create sprint <Icon icon="plus" className="size-4" />
<Icon
icon="plus"
className="size-4"
/>
</Button> </Button>
} }
sprints={sprints} sprints={sprints}
@@ -857,9 +772,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
)} )}
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Select a project to view sprints.</p>
Select a project to view sprints.
</p>
)} )}
</div> </div>
</div> </div>
@@ -900,16 +813,10 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="flex flex-col gap-2 max-h-86 overflow-y-scroll grid grid-cols-2"> <div className="flex flex-col gap-2 max-h-86 overflow-y-scroll grid grid-cols-2">
{Object.keys(statuses).map((status, index) => ( {Object.keys(statuses).map((status, index) => (
<div <div key={status} className="flex items-center justify-between p-2 border">
key={status}
className="flex items-center justify-between p-2 border"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm">{index + 1}</span> <span className="text-sm">{index + 1}</span>
<StatusTag <StatusTag status={status} colour={statuses[status]} />
status={status}
colour={statuses[status]}
/>
</div> </div>
{isAdmin && ( {isAdmin && (
<DropdownMenu> <DropdownMenu>
@@ -919,53 +826,29 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
noStyle noStyle
className="hover:opacity-80 cursor-pointer" className="hover:opacity-80 cursor-pointer"
> >
<Icon <Icon icon="ellipsisVertical" className="size-4 text-foreground" />
icon="ellipsisVertical"
className="size-4 text-foreground"
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent align="end" sideOffset={4} className="bg-background">
align="end"
sideOffset={4}
className="bg-background"
>
<DropdownMenuItem <DropdownMenuItem
disabled={index === 0} disabled={index === 0}
onSelect={() => onSelect={() => void moveStatus(status, "up")}
void moveStatus(status, "up")
}
className="hover:bg-primary-foreground" className="hover:bg-primary-foreground"
> >
<Icon <Icon icon="chevronUp" className="size-4 text-muted-foreground" />
icon="chevronUp"
className="size-4 text-muted-foreground"
/>
Move up Move up
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
disabled={ disabled={index === Object.keys(statuses).length - 1}
index === onSelect={() => void moveStatus(status, "down")}
Object.keys(statuses).length - 1
}
onSelect={() =>
void moveStatus(status, "down")
}
className="hover:bg-primary-foreground" className="hover:bg-primary-foreground"
> >
<Icon <Icon icon="chevronDown" className="size-4 text-muted-foreground" />
icon="chevronDown"
className="size-4 text-muted-foreground"
/>
Move down Move down
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
variant="destructive" variant="destructive"
disabled={ disabled={Object.keys(statuses).length <= 1}
Object.keys(statuses).length <= 1 onSelect={() => void handleRemoveStatusClick(status)}
}
onSelect={() =>
void handleRemoveStatusClick(status)
}
className="hover:bg-destructive/10" className="hover:bg-destructive/10"
> >
<Icon icon="x" className="size-4" /> <Icon icon="x" className="size-4" />
@@ -1012,19 +895,12 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
variant="outline" variant="outline"
size="md" size="md"
onClick={() => void handleCreateStatus()} onClick={() => void handleCreateStatus()}
disabled={ disabled={newStatusName.trim().length > ISSUE_STATUS_MAX_LENGTH}
newStatusName.trim().length >
ISSUE_STATUS_MAX_LENGTH
}
> >
<Icon icon="plus" className="size-4" /> <Icon icon="plus" className="size-4" />
</IconButton> </IconButton>
</div> </div>
{statusError && ( {statusError && <p className="text-xs text-destructive">{statusError}</p>}
<p className="text-xs text-destructive">
{statusError}
</p>
)}
</> </>
) : ( ) : (
<Button <Button
@@ -1045,9 +921,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
) : ( ) : (
<div className="flex flex-col gap-2 w-full min-w-0"> <div className="flex flex-col gap-2 w-full min-w-0">
<OrganisationSelect <OrganisationSelect
contentClass={ contentClass={"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"}
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
}
/> />
<p className="text-sm text-muted-foreground">No organisations yet.</p> <p className="text-sm text-muted-foreground">No organisations yet.</p>
</div> </div>
@@ -1083,8 +957,8 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
{statusToRemove ? ( {statusToRemove ? (
<StatusTag status={statusToRemove} colour={statuses[statusToRemove]} /> <StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />
) : null}{" "} ) : null}{" "}
status? <span className="font-700 text-foreground">{issuesUsingStatus}</span>{" "} status? <span className="font-700 text-foreground">{issuesUsingStatus}</span> issues are using
issues are using it. Which status would you like these issues to use instead? it. Which status would you like these issues to use instead?
</p> </p>
<Select value={reassignToStatus} onValueChange={setReassignToStatus}> <Select value={reassignToStatus} onValueChange={setReassignToStatus}>
<SelectTrigger className="w-min"> <SelectTrigger className="w-min">

View File

@@ -84,12 +84,7 @@ export function ProjectSelect({
<ProjectForm <ProjectForm
organisationId={selectedOrganisationId ?? undefined} organisationId={selectedOrganisationId ?? undefined}
trigger={ trigger={
<Button <Button size={"sm"} variant="ghost" className={"w-full"} disabled={!selectedOrganisationId}>
size={"sm"}
variant="ghost"
className={"w-full"}
disabled={!selectedOrganisationId}
>
Create Project Create Project
</Button> </Button>
} }

View File

@@ -113,11 +113,7 @@ export function ServerConfiguration({ trigger }: { trigger?: ReactNode }) {
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
{trigger || ( {trigger || (
<IconButton <IconButton size="lg" className="absolute top-2 right-2" title={"Server Configuration"}>
size="lg"
className="absolute top-2 right-2"
title={"Server Configuration"}
>
<Icon icon="serverIcon" className="size-4" /> <Icon icon="serverIcon" className="size-4" />
</IconButton> </IconButton>
)} )}
@@ -164,9 +160,7 @@ export function ServerConfiguration({ trigger }: { trigger?: ReactNode }) {
<Icon icon="undo2" className="size-4" /> <Icon icon="undo2" className="size-4" />
</IconButton> </IconButton>
</div> </div>
{!isValid && ( {!isValid && <Label className="text-destructive text-sm">Please enter a valid URL</Label>}
<Label className="text-destructive text-sm">Please enter a valid URL</Label>
)}
{healthError && <Label className="text-destructive text-sm">{healthError}</Label>} {healthError && <Label className="text-destructive text-sm">{healthError}</Label>}
</div> </div>
</div> </div>

View File

@@ -303,8 +303,7 @@ export function SprintForm({
type="submit" type="submit"
disabled={ disabled={
submitting || submitting ||
((name.trim() === "" || name.trim().length > SPRINT_NAME_MAX_LENGTH) && ((name.trim() === "" || name.trim().length > SPRINT_NAME_MAX_LENGTH) && submitAttempted) ||
submitAttempted) ||
(dateError !== "" && submitAttempted) (dateError !== "" && submitAttempted)
} }
> >

View File

@@ -83,10 +83,7 @@ function Calendar({
), ),
week: cn("flex w-full mt-2", defaultClassNames.week), week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header), week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header),
week_number: cn( week_number: cn("text-[0.8rem] select-none text-muted-foreground", defaultClassNames.week_number),
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number,
),
day: cn( day: cn(
"relative w-full h-full p-0 text-center group/day aspect-square select-none", "relative w-full h-full p-0 text-center group/day aspect-square select-none",
defaultClassNames.day, defaultClassNames.day,
@@ -95,10 +92,7 @@ function Calendar({
range_middle: cn(defaultClassNames.range_middle), range_middle: cn(defaultClassNames.range_middle),
range_end: cn("bg-accent", defaultClassNames.range_end), range_end: cn("bg-accent", defaultClassNames.range_end),
today: cn("border border-dashed -m-px", defaultClassNames.today), today: cn("border border-dashed -m-px", defaultClassNames.today),
outside: cn( outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside),
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside,
),
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled), disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn("invisible", defaultClassNames.hidden),
...classNames, ...classNames,
@@ -113,9 +107,7 @@ function Calendar({
} }
if (orientation === "right") { if (orientation === "right") {
return ( return <Icon icon="chevronRightIcon" className={cn("size-4", className)} {...props} />;
<Icon icon="chevronRightIcon" className={cn("size-4", className)} {...props} />
);
} }
return <Icon icon="chevronDownIcon" className={cn("size-4", className)} {...props} />; return <Icon icon="chevronDownIcon" className={cn("size-4", className)} {...props} />;
@@ -171,10 +163,7 @@ function CalendarDayButton({
size="icon" size="icon"
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
data-selected-single={ data-selected-single={
modifiers.selected && modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
} }
data-range-start={modifiers.range_start} data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}

View File

@@ -18,11 +18,7 @@ export default function ColourPicker({
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild={asChild}> <PopoverTrigger asChild={asChild}>
<Button <Button type="button" className={cn("w-8 h-8", className)} style={{ backgroundColor: colour }} />
type="button"
className={cn("w-8 h-8", className)}
style={{ backgroundColor: colour }}
/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-fit grid gap-2 p-2" align="start" side={"top"}> <PopoverContent className="w-fit grid gap-2 p-2" align="start" side={"top"}>
<HexColorPicker color={colour} onChange={onChange} className="p-0 m-0" /> <HexColorPicker color={colour} onChange={onChange} className="p-0 m-0" />

View File

@@ -36,9 +36,7 @@ function IconButton({
size, size,
...props ...props
}: React.ComponentProps<"button"> & VariantProps<typeof iconButtonVariants>) { }: React.ComponentProps<"button"> & VariantProps<typeof iconButtonVariants>) {
return ( return <button type="button" className={cn(iconButtonVariants({ variant, size, className }))} {...props} />;
<button type="button" className={cn(iconButtonVariants({ variant, size, className }))} {...props} />
);
} }
export { IconButton, iconButtonVariants }; export { IconButton, iconButtonVariants };

View File

@@ -15,9 +15,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return ( return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
<tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />
);
} }
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {

View File

@@ -4,9 +4,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return ( return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />;
<TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />
);
} }
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) { function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {

View File

@@ -40,14 +40,9 @@ export function UploadAvatar({
onAvatarUploaded(url); onAvatarUploaded(url);
setUploading(false); setUploading(false);
toast.success( toast.success(<div className="flex flex-col items-center gap-4">Avatar uploaded successfully</div>, {
<div className="flex flex-col items-center gap-4">
Avatar uploaded successfully
</div>,
{
dismissible: false, dismissible: false,
}, });
);
} catch (err) { } catch (err) {
const message = parseError(err as Error); const message = parseError(err as Error);
setError(message); setError(message);

View File

@@ -42,9 +42,7 @@ export function UploadOrgIcon({
setUploading(false); setUploading(false);
toast.success( toast.success(
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">Organisation icon uploaded successfully</div>,
Organisation icon uploaded successfully
</div>,
{ {
dismissible: false, dismissible: false,
}, },

View File

@@ -16,9 +16,7 @@ export default function Landing() {
{!isLoading && user ? ( {!isLoading && user ? (
<> <>
{user && ( {user && (
<h1 className="text-xl font-basteleur font-400"> <h1 className="text-xl font-basteleur font-400">Welcome back {user.name.split(" ")[0]}!</h1>
Welcome back {user.name.split(" ")[0]}!
</h1>
)} )}
<Button asChild variant="outline" size="sm"> <Button asChild variant="outline" size="sm">
<Link to="/app">Open app</Link> <Link to="/app">Open app</Link>
@@ -34,9 +32,7 @@ export default function Landing() {
<main className="flex-1 flex flex-col items-center justify-center gap-8"> <main className="flex-1 flex flex-col items-center justify-center gap-8">
<div className="max-w-3xl text-center space-y-4"> <div className="max-w-3xl text-center space-y-4">
<h1 className="text-[54px] font-basteleur font-700"> <h1 className="text-[54px] font-basteleur font-700">Need a snappy project management tool?</h1>
Need a snappy project management tool?
</h1>
<p className="text-[24px] font-goudy text-muted-foreground"> <p className="text-[24px] font-goudy text-muted-foreground">
Build your next project with <span className="font-goudy font-700">Sprint.</span> Build your next project with <span className="font-goudy font-700">Sprint.</span>
</p> </p>

View File

@@ -245,11 +245,7 @@ export default function Timeline() {
const barStyle = getSprintBarStyle(sprint); const barStyle = getSprintBarStyle(sprint);
const showTodayLabel = sprintIndex === 0; const showTodayLabel = sprintIndex === 0;
return ( return (
<div <div key={sprint.id} className="grid border-b" style={{ gridTemplateColumns }}>
key={sprint.id}
className="grid border-b"
style={{ gridTemplateColumns }}
>
<div <div
className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`} className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`}
> >
@@ -267,9 +263,7 @@ export default function Timeline() {
{getSprintDateRange(sprint)} {getSprintDateRange(sprint)}
</div> </div>
{sprintIssues.length === 0 && ( {sprintIssues.length === 0 && (
<div className="text-xs text-muted-foreground text-pretty"> <div className="text-xs text-muted-foreground text-pretty">No issues assigned.</div>
No issues assigned.
</div>
)} )}
{sprintIssues.length > 0 && ( {sprintIssues.length > 0 && (
<div className={`flex flex-col gap-${BREATHING_ROOM}`}> <div className={`flex flex-col gap-${BREATHING_ROOM}`}>
@@ -277,30 +271,21 @@ export default function Timeline() {
<IssueLine <IssueLine
key={issue.Issue.id} key={issue.Issue.id}
issue={issue} issue={issue}
statusColour={ statusColour={statuses[issue.Issue.status] ?? DEFAULT_STATUS_COLOUR}
statuses[issue.Issue.status] ??
DEFAULT_STATUS_COLOUR
}
/> />
))} ))}
</div> </div>
)} )}
</div> </div>
<div <div
className={cn( className={cn(`py-${BREATHING_ROOM} relative min-h-12`, "border-l")}
`py-${BREATHING_ROOM} relative min-h-12`,
"border-l",
)}
style={{ gridColumn: "2 / -1" }} style={{ gridColumn: "2 / -1" }}
> >
<div className="absolute inset-0 flex z-10 pointer-events-none"> <div className="absolute inset-0 flex z-10 pointer-events-none">
{weeks.map((week, index) => ( {weeks.map((week, index) => (
<div <div
key={`${week.toISOString()}-${sprint.id}`} key={`${week.toISOString()}-${sprint.id}`}
className={cn( className={cn("flex-1", index === 0 ? "" : "border-l")}
"flex-1",
index === 0 ? "" : "border-l",
)}
/> />
))} ))}
</div> </div>
@@ -344,13 +329,9 @@ export default function Timeline() {
<div <div
className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`} className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`}
> >
<div className="text-sm font-medium text-muted-foreground"> <div className="text-sm font-medium text-muted-foreground">Backlog</div>
Backlog
</div>
{issueGroup.unassigned.length === 0 && ( {issueGroup.unassigned.length === 0 && (
<div className="text-xs text-muted-foreground text-pretty"> <div className="text-xs text-muted-foreground text-pretty">No unassigned issues.</div>
No unassigned issues.
</div>
)} )}
{issueGroup.unassigned.length > 0 && ( {issueGroup.unassigned.length > 0 && (
<div className={`flex flex-col gap-${BREATHING_ROOM}`}> <div className={`flex flex-col gap-${BREATHING_ROOM}`}>
@@ -358,10 +339,7 @@ export default function Timeline() {
<IssueLine <IssueLine
key={issue.Issue.id} key={issue.Issue.id}
issue={issue} issue={issue}
statusColour={ statusColour={statuses[issue.Issue.status] ?? DEFAULT_STATUS_COLOUR}
statuses[issue.Issue.status] ??
DEFAULT_STATUS_COLOUR
}
/> />
))} ))}
</div> </div>