mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
Merge pull request #2 from hex248/development
Made better use of --personality colour
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { Issue, Organisation, OrganisationMember, Project, User } from "@sprint/shared";
|
import {
|
||||||
|
DEFAULT_ISSUE_TYPES,
|
||||||
|
DEFAULT_STATUS_COLOURS,
|
||||||
|
Issue,
|
||||||
|
IssueAssignee,
|
||||||
|
IssueComment,
|
||||||
|
Organisation,
|
||||||
|
OrganisationMember,
|
||||||
|
Project,
|
||||||
|
Sprint,
|
||||||
|
User,
|
||||||
|
} from "@sprint/shared";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
|
||||||
@@ -66,6 +77,24 @@ const issues = [
|
|||||||
{ title: "Add batch processing", description: "Need to process large datasets efficiently." },
|
{ title: "Add batch processing", description: "Need to process large datasets efficiently." },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const issueStatuses = Object.keys(DEFAULT_STATUS_COLOURS);
|
||||||
|
const issueTypes = Object.keys(DEFAULT_ISSUE_TYPES);
|
||||||
|
|
||||||
|
const issueComments = [
|
||||||
|
"started looking into this, will share updates soon",
|
||||||
|
"i can reproduce this on staging",
|
||||||
|
"adding details in the description",
|
||||||
|
"should be a small fix, pairing with u2",
|
||||||
|
"blocked on api response shape",
|
||||||
|
"added logs, issue still happening",
|
||||||
|
"fix is ready for review",
|
||||||
|
"can we confirm expected behavior?",
|
||||||
|
"this seems related to recent deploy",
|
||||||
|
"i will take this one",
|
||||||
|
"qa verified on latest build",
|
||||||
|
"needs product input before proceeding",
|
||||||
|
];
|
||||||
|
|
||||||
const passwordHash = await hashPassword("a");
|
const passwordHash = await hashPassword("a");
|
||||||
const users = [
|
const users = [
|
||||||
{ name: "user 1", username: "u1", passwordHash, avatarURL: null },
|
{ name: "user 1", username: "u1", passwordHash, avatarURL: null },
|
||||||
@@ -146,14 +175,48 @@ async function seed() {
|
|||||||
|
|
||||||
console.log(`created ${projects.length} projects`);
|
console.log(`created ${projects.length} projects`);
|
||||||
|
|
||||||
// create 3-6 issues per project
|
console.log("creating sprints...");
|
||||||
|
const sprintValues = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const project of projects) {
|
||||||
|
sprintValues.push(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
name: "Sprint 1",
|
||||||
|
color: "#3b82f6",
|
||||||
|
startDate: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000),
|
||||||
|
endDate: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
name: "Sprint 2",
|
||||||
|
color: "#22c55e",
|
||||||
|
startDate: new Date(now.getTime()),
|
||||||
|
endDate: new Date(now.getTime() + 13 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdSprints = await db.insert(Sprint).values(sprintValues).returning();
|
||||||
|
const sprintsByProject = new Map<number, (typeof createdSprints)[number][]>();
|
||||||
|
|
||||||
|
for (const sprint of createdSprints) {
|
||||||
|
const list = sprintsByProject.get(sprint.projectId) ?? [];
|
||||||
|
list.push(sprint);
|
||||||
|
sprintsByProject.set(sprint.projectId, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`created ${createdSprints.length} sprints`);
|
||||||
|
|
||||||
|
// create 6-12 issues per project
|
||||||
console.log("creating issues...");
|
console.log("creating issues...");
|
||||||
const allUsers = [u1, u2];
|
const allUsers = [u1, u2];
|
||||||
const issueValues = [];
|
const issueValues = [];
|
||||||
let issueIndex = 0;
|
let issueIndex = 0;
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const numIssues = Math.floor(Math.random() * 4) + 3; // 3-6 issues
|
const numIssues = Math.floor(Math.random() * 7) + 6; // 6-12 issues
|
||||||
for (let i = 1; i <= numIssues; i++) {
|
for (let i = 1; i <= numIssues; i++) {
|
||||||
const creator = allUsers[Math.floor(Math.random() * allUsers.length)];
|
const creator = allUsers[Math.floor(Math.random() * allUsers.length)];
|
||||||
if (!creator) {
|
if (!creator) {
|
||||||
@@ -164,20 +227,89 @@ async function seed() {
|
|||||||
throw new Error("failed to select issue");
|
throw new Error("failed to select issue");
|
||||||
}
|
}
|
||||||
issueIndex++;
|
issueIndex++;
|
||||||
|
const status = issueStatuses[Math.floor(Math.random() * issueStatuses.length)];
|
||||||
|
const type = issueTypes[Math.floor(Math.random() * issueTypes.length)];
|
||||||
|
if (!status || !type) {
|
||||||
|
throw new Error("failed to select issue status or type");
|
||||||
|
}
|
||||||
|
const projectSprints = sprintsByProject.get(project.id);
|
||||||
|
if (!projectSprints || projectSprints.length === 0) {
|
||||||
|
throw new Error("failed to select project sprint");
|
||||||
|
}
|
||||||
|
const sprint = projectSprints[Math.floor(Math.random() * projectSprints.length)];
|
||||||
|
if (!sprint) {
|
||||||
|
throw new Error("failed to select sprint");
|
||||||
|
}
|
||||||
|
|
||||||
issueValues.push({
|
issueValues.push({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
number: i,
|
number: i,
|
||||||
title: issue.title,
|
title: issue.title,
|
||||||
description: issue.description,
|
description: issue.description,
|
||||||
|
status,
|
||||||
|
type,
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
|
sprintId: sprint.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(Issue).values(issueValues);
|
const createdIssues = await db.insert(Issue).values(issueValues).returning();
|
||||||
|
|
||||||
console.log(`created ${issueValues.length} issues`);
|
console.log(`created ${createdIssues.length} issues`);
|
||||||
|
|
||||||
|
console.log("creating issue assignees...");
|
||||||
|
const assigneeValues = [];
|
||||||
|
|
||||||
|
for (const issue of createdIssues) {
|
||||||
|
const assigneeCount = Math.floor(Math.random() * 3);
|
||||||
|
const picked = new Set<number>();
|
||||||
|
for (let i = 0; i < assigneeCount; i++) {
|
||||||
|
const assignee = usersDB[Math.floor(Math.random() * usersDB.length)];
|
||||||
|
if (!assignee) {
|
||||||
|
throw new Error("failed to select issue assignee");
|
||||||
|
}
|
||||||
|
if (picked.has(assignee.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
picked.add(assignee.id);
|
||||||
|
assigneeValues.push({
|
||||||
|
issueId: issue.id,
|
||||||
|
userId: assignee.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assigneeValues.length > 0) {
|
||||||
|
await db.insert(IssueAssignee).values(assigneeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`created ${assigneeValues.length} issue assignees`);
|
||||||
|
|
||||||
|
console.log("creating issue comments...");
|
||||||
|
const commentValues = [];
|
||||||
|
|
||||||
|
for (const issue of createdIssues) {
|
||||||
|
const commentCount = Math.floor(Math.random() * 3);
|
||||||
|
for (let i = 0; i < commentCount; i++) {
|
||||||
|
const commenter = usersDB[Math.floor(Math.random() * usersDB.length)];
|
||||||
|
const comment = issueComments[Math.floor(Math.random() * issueComments.length)];
|
||||||
|
if (!commenter || !comment) {
|
||||||
|
throw new Error("failed to select issue comment data");
|
||||||
|
}
|
||||||
|
commentValues.push({
|
||||||
|
issueId: issue.id,
|
||||||
|
userId: commenter.id,
|
||||||
|
body: comment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commentValues.length > 0) {
|
||||||
|
await db.insert(IssueComment).values(commentValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`created ${commentValues.length} issue comments`);
|
||||||
|
|
||||||
console.log("database seeding complete");
|
console.log("database seeding complete");
|
||||||
console.log("\ndemo accounts (password: a):");
|
console.log("\ndemo accounts (password: a):");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const main = async () => {
|
|||||||
|
|
||||||
"/user/by-username": withGlobal(withAuth(routes.userByUsername)),
|
"/user/by-username": withGlobal(withAuth(routes.userByUsername)),
|
||||||
"/user/update": withGlobal(withAuth(withCSRF(routes.userUpdate))),
|
"/user/update": withGlobal(withAuth(withCSRF(routes.userUpdate))),
|
||||||
"/user/upload-avatar": withGlobal(withAuth(withCSRF(routes.userUploadAvatar))),
|
"/user/upload-avatar": withGlobal(routes.userUploadAvatar),
|
||||||
|
|
||||||
"/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))),
|
"/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))),
|
||||||
"/issue/by-id": withGlobal(withAuth(routes.issueById)),
|
"/issue/by-id": withGlobal(withAuth(routes.issueById)),
|
||||||
|
|||||||
@@ -66,10 +66,10 @@
|
|||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(61.275% 0.20731 24.986);
|
||||||
--border: oklch(73.802% 0.00008 271.152);
|
--border: oklch(73.802% 0.00008 271.152);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: var(--personality);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(100% 0.00011 271.152 / 0.22);
|
--border: oklch(100% 0.00011 271.152 / 0.22);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: var(--personality);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
@@ -132,6 +132,10 @@
|
|||||||
background-color: var(--personality);
|
background-color: var(--personality);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
}
|
}
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 1px solid var(--personality);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default function Avatar({
|
|||||||
size,
|
size,
|
||||||
textClass = "text-xs",
|
textClass = "text-xs",
|
||||||
strong = false,
|
strong = false,
|
||||||
|
skipOrgCheck = false,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
avatarURL?: string | null;
|
avatarURL?: string | null;
|
||||||
@@ -57,6 +58,7 @@ export default function Avatar({
|
|||||||
size?: number;
|
size?: number;
|
||||||
textClass?: string;
|
textClass?: string;
|
||||||
strong?: boolean;
|
strong?: boolean;
|
||||||
|
skipOrgCheck?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
// if the username matches the authed user, use their avatarURL and name (avoid stale data)
|
// if the username matches the authed user, use their avatarURL and name (avoid stale data)
|
||||||
@@ -69,13 +71,15 @@ export default function Avatar({
|
|||||||
? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)]
|
? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)]
|
||||||
: "bg-muted";
|
: "bg-muted";
|
||||||
|
|
||||||
|
const showAvatar = skipOrgCheck || selectedOrganisation?.Organisation.features.userAvatars;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center rounded-full",
|
"flex items-center justify-center rounded-full",
|
||||||
"text-white font-medium select-none",
|
"text-white font-medium select-none",
|
||||||
name && "border",
|
name && "border",
|
||||||
(!avatarURL || !selectedOrganisation?.Organisation.features.userAvatars) && backgroundClass,
|
(!avatarURL || !showAvatar) && backgroundClass,
|
||||||
|
|
||||||
"transition-colors",
|
"transition-colors",
|
||||||
`w-${size || 6}`,
|
`w-${size || 6}`,
|
||||||
@@ -83,7 +87,7 @@ export default function Avatar({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedOrganisation?.Organisation.features.userAvatars && avatarURL ? (
|
{showAvatar && avatarURL ? (
|
||||||
<img
|
<img
|
||||||
src={avatarURL}
|
src={avatarURL}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ export function IssueComments({ issueId, className }: { issueId: number; classNa
|
|||||||
</div>
|
</div>
|
||||||
{isAuthor ? (
|
{isAuthor ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleDelete(comment)}
|
onClick={() => handleDelete(comment)}
|
||||||
disabled={deletingId === comment.Comment.id}
|
disabled={deletingId === comment.Comment.id}
|
||||||
title="Delete comment"
|
title="Delete comment"
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ export function IssueDetails({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{organisation?.Organisation.features.description &&
|
{organisation?.Organisation.features.issueDescriptions &&
|
||||||
(description || isEditingDescription ? (
|
(description || isEditingDescription ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={descriptionRef}
|
ref={descriptionRef}
|
||||||
|
|||||||
@@ -147,32 +147,33 @@ export function IssuesTable({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showId = columns.id == null || columns.id === true;
|
||||||
|
const showTitle = columns.title == null || columns.title === true;
|
||||||
|
const showDescription = columns.description == null || columns.description === true;
|
||||||
|
const showAssignee = columns.assignee == null || columns.assignee === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className={cn("table-fixed", className)}>
|
<Table className={cn("table-fixed", className)}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow hoverEffect={false} className="bg-secondary">
|
<TableRow hoverEffect={false} className="bg-secondary">
|
||||||
{(columns.id == null || columns.id === true) && (
|
{showId && (
|
||||||
<TableHead className="text-right w-10 border-r text-xs font-medium text-muted-foreground">
|
<TableHead className="text-right w-10 border-r text-xs font-medium text-muted-foreground">
|
||||||
ID
|
ID
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
{(columns.title == null || columns.title === true) && (
|
{showTitle && <TableHead className="text-xs font-medium text-muted-foreground">Title</TableHead>}
|
||||||
<TableHead className="text-xs font-medium text-muted-foreground">Title</TableHead>
|
{showDescription && (
|
||||||
)}
|
|
||||||
{(columns.description == null || columns.description === true) && (
|
|
||||||
<TableHead className="text-xs font-medium text-muted-foreground">Description</TableHead>
|
<TableHead className="text-xs font-medium text-muted-foreground">Description</TableHead>
|
||||||
)}
|
)}
|
||||||
{/* below is kept blank to fill the space, used as the "Assignee" column */}
|
{/* below is kept blank to fill the space, used as the "Assignee" column */}
|
||||||
{(columns.assignee == null || columns.assignee === true) && (
|
{showAssignee && <TableHead className="w-[1%]"></TableHead>}
|
||||||
<TableHead className="w-[1%]"></TableHead>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{issues.map((issueData) => (
|
{issues.map((issueData) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={issueData.Issue.id}
|
key={issueData.Issue.id}
|
||||||
className="cursor-pointer max-w-full"
|
className={cn("cursor-pointer max-w-full")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (issueData.Issue.id === selectedIssueId) {
|
if (issueData.Issue.id === selectedIssueId) {
|
||||||
selectIssue(null);
|
selectIssue(null);
|
||||||
@@ -181,8 +182,13 @@ export function IssuesTable({
|
|||||||
selectIssue(issueData);
|
selectIssue(issueData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(columns.id == null || columns.id === true) && (
|
{showId && (
|
||||||
<TableCell className="font-medium border-r text-right p-0">
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"font-medium border-r text-right p-0",
|
||||||
|
issueData.Issue.id === selectedIssueId && "shadow-[inset_2px_0_0_0_var(--personality)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
href={getIssueUrl(issueData.Issue.number)}
|
||||||
onClick={handleLinkClick}
|
onClick={handleLinkClick}
|
||||||
@@ -192,8 +198,15 @@ export function IssuesTable({
|
|||||||
</a>
|
</a>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{(columns.title == null || columns.title === true) && (
|
{showTitle && (
|
||||||
<TableCell className="min-w-0 p-0">
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 p-0",
|
||||||
|
!showId &&
|
||||||
|
issueData.Issue.id === selectedIssueId &&
|
||||||
|
"shadow-[inset_2px_0_0_0_var(--personality)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
href={getIssueUrl(issueData.Issue.number)}
|
||||||
onClick={handleLinkClick}
|
onClick={handleLinkClick}
|
||||||
@@ -215,7 +228,7 @@ export function IssuesTable({
|
|||||||
</a>
|
</a>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{(columns.description == null || columns.description === true) && (
|
{showDescription && (
|
||||||
<TableCell className="overflow-hidden p-0">
|
<TableCell className="overflow-hidden p-0">
|
||||||
<a
|
<a
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
href={getIssueUrl(issueData.Issue.number)}
|
||||||
@@ -226,7 +239,7 @@ export function IssuesTable({
|
|||||||
</a>
|
</a>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{(columns.assignee == null || columns.assignee === true) && (
|
{showAssignee && (
|
||||||
<TableCell className="h-[32px] p-0">
|
<TableCell className="h-[32px] p-0">
|
||||||
<a
|
<a
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
href={getIssueUrl(issueData.Issue.number)}
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ export default function LogInForm() {
|
|||||||
username={username || undefined}
|
username={username || undefined}
|
||||||
avatarURL={avatarURL}
|
avatarURL={avatarURL}
|
||||||
onAvatarUploaded={setAvatarUrl}
|
onAvatarUploaded={setAvatarUrl}
|
||||||
|
skipOrgCheck
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
{avatarURL && (
|
{avatarURL && (
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="trash" className="size-4" />
|
<Icon icon="trash" className="size-4" color="white" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -872,7 +872,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="trash" className="size-4" />
|
<Icon icon="trash" className="size-4" color="white" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -951,7 +951,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
className="hover:bg-destructive/10"
|
className="hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
<Icon icon="trash" className="size-4" />
|
<Icon icon="trash" className="size-4" color="white" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -75,11 +75,9 @@ export function TimerControls({
|
|||||||
<div className={cn("ml-auto flex items-center", isCompact ? "gap-1" : "gap-2")}>
|
<div className={cn("ml-auto flex items-center", isCompact ? "gap-1" : "gap-2")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
variant="dummy"
|
|
||||||
aria-label={running ? "Pause timer" : "Resume timer"}
|
aria-label={running ? "Pause timer" : "Resume timer"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
className={"hover:opacity-70"}
|
|
||||||
>
|
>
|
||||||
{running ? (
|
{running ? (
|
||||||
<Icon icon="pause" size={isCompact ? 14 : 16} />
|
<Icon icon="pause" size={isCompact ? 14 : 16} />
|
||||||
@@ -87,14 +85,7 @@ export function TimerControls({
|
|||||||
<Icon icon="play" size={isCompact ? 14 : 16} />
|
<Icon icon="play" size={isCompact ? 14 : 16} />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton size={"sm"} aria-label="End timer" disabled={disabled || !hasTimer} onClick={handleEnd}>
|
||||||
size={"sm"}
|
|
||||||
variant="destructive"
|
|
||||||
aria-label="End timer"
|
|
||||||
disabled={disabled || !hasTimer}
|
|
||||||
onClick={handleEnd}
|
|
||||||
className={"hover:opacity-70"}
|
|
||||||
>
|
|
||||||
<Icon icon="stop" size={isCompact ? 14 : 16} color={"var(--destructive)"} />
|
<Icon icon="stop" size={isCompact ? 14 : 16} color={"var(--destructive)"} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white hover:bg-destructive/80 active:bg-destructive/70 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:hover:bg-destructive/70",
|
||||||
outline: "bg-transparent border dark:hover:bg-muted/40",
|
outline: "bg-transparent border dark:hover:bg-muted/40",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer ring-offset-background focus:ring-ring",
|
"cursor-pointer",
|
||||||
"data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
"data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||||
"absolute opacity-70",
|
"absolute opacity-70",
|
||||||
closePos === "top-left" && "top-4 left-4",
|
closePos === "top-left" && "top-4 left-4",
|
||||||
closePos === "top-right" && "top-4 right-4",
|
closePos === "top-right" && "top-4 right-4",
|
||||||
closePos === "bottom-left" && "bottom-4 left-4",
|
closePos === "bottom-left" && "bottom-4 left-4",
|
||||||
closePos === "bottom-right" && "bottom-4 right-4",
|
closePos === "bottom-right" && "bottom-4 right-4",
|
||||||
"hover:opacity-100 focus:ring-2 focus:ring-offset-2 ",
|
"hover:opacity-100",
|
||||||
"ocus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none",
|
"ocus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none",
|
||||||
"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const iconButtonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "hover:text-foreground/70",
|
default: "hover:text-foreground/70 hover:opacity-70",
|
||||||
destructive: "text-destructive hover:text-destructive/70",
|
destructive: "text-destructive hover:opacity-70",
|
||||||
yellow: "text-yellow-500 hover:text-yellow-500/70",
|
yellow: "text-yellow-500 hover:text-yellow-500/70",
|
||||||
green: "text-green-500 hover:text-green-500/70",
|
green: "text-green-500 hover:text-green-500/70",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function Input({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"border-input dark:bg-input/30 flex h-9 w-full min-w-0 items-center border bg-transparent",
|
"border-input dark:bg-input/30 flex h-9 w-full min-w-0 items-center border bg-transparent",
|
||||||
"transition-[color,box-shadow]",
|
"transition-[color,box-shadow]",
|
||||||
"has-[:focus-visible]:border-ring",
|
"has-[:focus-visible]:border-[var(--personality)] ",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
"aria-invalid:border-destructive",
|
"aria-invalid:border-destructive",
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function Switch({
|
|||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"peer data-[state=checked]:bg-personality data-[state=unchecked]:bg-input",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80",
|
"focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80",
|
||||||
"group/switch inline-flex shrink-0 items-center rounded-full border border-transparent",
|
"group/switch inline-flex shrink-0 items-center rounded-full border border-transparent",
|
||||||
"outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
@@ -27,7 +27,7 @@ function Switch({
|
|||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
data-slot="switch-thumb"
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
"bg-background dark:data-[state=unchecked]:bg-personality dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function UploadAvatar({
|
|||||||
username,
|
username,
|
||||||
avatarURL,
|
avatarURL,
|
||||||
onAvatarUploaded,
|
onAvatarUploaded,
|
||||||
|
skipOrgCheck = false,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -20,6 +21,7 @@ export function UploadAvatar({
|
|||||||
avatarURL?: string | null;
|
avatarURL?: string | null;
|
||||||
onAvatarUploaded: (avatarURL: string) => void;
|
onAvatarUploaded: (avatarURL: string) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
skipOrgCheck?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -71,6 +73,7 @@ export function UploadAvatar({
|
|||||||
size={24}
|
size={24}
|
||||||
textClass={"text-4xl"}
|
textClass={"text-4xl"}
|
||||||
strong
|
strong
|
||||||
|
skipOrgCheck={skipOrgCheck}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!uploading && showEdit && (
|
{!uploading && showEdit && (
|
||||||
|
|||||||
@@ -330,13 +330,13 @@ export default function Timeline() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-y-0 w-px bg-primary",
|
"absolute inset-y-0 w-px bg-[var(--personality)]",
|
||||||
showTodayLabel && "mt-1",
|
showTodayLabel && "mt-1",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{showTodayLabel && (
|
{showTodayLabel && (
|
||||||
<div className="absolute -top-1.5">
|
<div className="absolute -top-1.5">
|
||||||
<span className="bg-primary px-1 py-0.5 text-[10px] font-semibold text-primary-foreground whitespace-nowrap">
|
<span className="bg-[var(--personality)] px-1 py-0.5 text-[10px] font-semibold text-[var(--background)] whitespace-nowrap">
|
||||||
TODAY
|
TODAY
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user