diff --git a/components.json b/components.json
index 0da679a..baef07e 100644
--- a/components.json
+++ b/components.json
@@ -6,7 +6,7 @@
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
- "baseColor": "zinc",
+ "baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
diff --git a/package.json b/package.json
index da4b229..8b9f649 100644
--- a/package.json
+++ b/package.json
@@ -22,9 +22,14 @@
"dependencies": {
"@clerk/nextjs": "^6.5.1",
"@clerk/themes": "^2.1.48",
+ "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-avatar": "^1.1.1",
+ "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.1",
"@sentry/nextjs": "^8",
"@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0",
@@ -37,6 +42,7 @@
"posthog-js": "^1.194.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hook-form": "^7.53.2",
"server-only": "^0.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e9a3d71..7b4b7db 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,15 +14,30 @@ importers:
'@clerk/themes':
specifier: ^2.1.48
version: 2.1.48
+ '@hookform/resolvers':
+ specifier: ^3.9.1
+ version: 3.9.1(react-hook-form@7.53.2(react@18.3.1))
'@radix-ui/react-avatar':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-label':
+ specifier: ^2.1.0
+ version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-navigation-menu':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot':
+ specifier: ^1.1.0
+ version: 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-switch':
+ specifier: ^1.1.1
+ version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@sentry/nextjs':
specifier: ^8
version: 8.41.0(@opentelemetry/core@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.96.1(esbuild@0.19.12))
@@ -59,6 +74,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.53.2
+ version: 7.53.2(react@18.3.1)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -532,6 +550,11 @@ packages:
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ '@hookform/resolvers@3.9.1':
+ resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'}
@@ -994,6 +1017,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-checkbox@1.1.2':
+ resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
@@ -1100,6 +1136,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-label@2.1.0':
+ resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-navigation-menu@1.2.1':
resolution: {integrity: sha512-egDo0yJD2IK8L17gC82vptkvW1jLeni1VuqCyzY727dSJdk5cDjINomouLoNk8RVF7g2aNIfENKWL4UzeU9c8Q==}
peerDependencies:
@@ -1161,6 +1210,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-switch@1.1.1':
+ resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@@ -1206,6 +1268,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-size@1.1.0':
+ resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-visually-hidden@1.1.0':
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
peerDependencies:
@@ -2989,6 +3060,12 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-hook-form@7.53.2:
+ resolution: {integrity: sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -3834,6 +3911,10 @@ snapshots:
'@eslint/js@8.57.1': {}
+ '@hookform/resolvers@3.9.1(react-hook-form@7.53.2(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.53.2(react@18.3.1)
+
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -4315,6 +4396,22 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
+ '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -4410,6 +4507,15 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
+ '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-navigation-menu@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -4468,6 +4574,21 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
+ '@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -4500,6 +4621,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
+ '@radix-ui/react-use-size@1.1.0(@types/react@18.3.12)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.12
+
'@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -6431,6 +6559,10 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-hook-form@7.53.2(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
react-is@16.13.1: {}
react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):
diff --git a/src/app/@modal/(.)timer/[id]/modal.tsx b/src/app/@modal/(.)timer/[id]/modal.tsx
index 4481ebb..398d6a1 100644
--- a/src/app/@modal/(.)timer/[id]/modal.tsx
+++ b/src/app/@modal/(.)timer/[id]/modal.tsx
@@ -1,5 +1,5 @@
"use client";
-import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { useRouter } from "next/navigation";
export default function Modal({ children }: { children: React.ReactNode }) {
diff --git a/src/app/@modal/(.)timer/[id]/page.tsx b/src/app/@modal/(.)timer/[id]/page.tsx
index 652db6f..59a02c9 100644
--- a/src/app/@modal/(.)timer/[id]/page.tsx
+++ b/src/app/@modal/(.)timer/[id]/page.tsx
@@ -1,17 +1,26 @@
-import FullPageTimer from "~/app/_components/full-page-timer";
-import { getTimer } from "~/server/queries";
+import FullPageTimer from "@/app/_components/full-page-timer";
+import { getTimer } from "@/server/queries";
import Modal from "./modal";
+import { useAuth } from "@clerk/nextjs";
export default async function TimerModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
+ const user = useAuth();
+
const timer = await getTimer((await params).id);
+ const owner = timer.userEmail === user.userId;
+
return (
-
+
);
}
diff --git a/src/app/_analytics/providers.tsx b/src/app/_analytics/providers.tsx
index c68c3ee..41abbc6 100644
--- a/src/app/_analytics/providers.tsx
+++ b/src/app/_analytics/providers.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useEffect } from "react";
+import { useAuth, useUser } from "@clerk/nextjs";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
@@ -9,6 +11,28 @@ if (typeof window !== "undefined") {
});
}
+function PostHogAuthWrapper({ children }: { children: React.ReactNode }) {
+ const auth = useAuth();
+ const userInfo = useUser();
+
+ useEffect(() => {
+ if (userInfo.user) {
+ posthog.identify(userInfo.user.id, {
+ email: userInfo.user.emailAddresses[0]?.emailAddress,
+ name: userInfo.user.fullName,
+ });
+ } else if (!auth.isSignedIn) {
+ posthog.reset();
+ }
+ }, [auth, userInfo]);
+
+ return children;
+}
+
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
- return {children};
+ return (
+
+ {children}
+
+ );
}
diff --git a/src/app/_components/full-page-timer.tsx b/src/app/_components/full-page-timer.tsx
index 72dd7ab..83ff21e 100644
--- a/src/app/_components/full-page-timer.tsx
+++ b/src/app/_components/full-page-timer.tsx
@@ -36,6 +36,7 @@ const calculateTimeSince = (start: Date | null) => {
export default function Timer(props: {
eventTime: Date | null;
caption: string;
+ owner: boolean;
}) {
const [timeSince, setTimeSince] = useState(
calculateTimeSince(props.eventTime),
@@ -53,7 +54,7 @@ export default function Timer(props: {
const formattedTime = formatTime(timeSince);
return (
-
+
@@ -91,9 +92,10 @@ export default function Timer(props: {
SECONDS
-
);
}
diff --git a/src/app/_components/navigation.tsx b/src/app/_components/navigation.tsx
index 814d992..7adaa81 100644
--- a/src/app/_components/navigation.tsx
+++ b/src/app/_components/navigation.tsx
@@ -1,24 +1,47 @@
import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
+import Link from "next/link";
import {
NavigationMenu,
+ NavigationMenuItem,
NavigationMenuList,
-} from "~/components/ui/navigation-menu";
+ NavigationMenuLink,
+ navigationMenuTriggerStyle,
+} from "@/components/ui/navigation-menu";
export function TopNav() {
return (
);
}
diff --git a/src/app/_components/timer.tsx b/src/app/_components/timer.tsx
index 58b93e5..56bf910 100644
--- a/src/app/_components/timer.tsx
+++ b/src/app/_components/timer.tsx
@@ -7,8 +7,8 @@ import {
CardDescription,
CardHeader,
CardTitle,
-} from "~/components/ui/card";
-import { Avatar, AvatarImage } from "~/components/ui/avatar";
+} from "@/components/ui/card";
+import { Avatar, AvatarImage } from "@/components/ui/avatar";
const formatTime = (elapsed: number | null) => {
if (elapsed === null) {
diff --git a/src/app/api/sentry-example-api/route.ts b/src/app/api/sentry-example-api/route.ts
deleted file mode 100644
index f486f3d..0000000
--- a/src/app/api/sentry-example-api/route.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { NextResponse } from "next/server";
-
-export const dynamic = "force-dynamic";
-
-// A faulty API route to test Sentry's error monitoring
-export function GET() {
- throw new Error("Sentry Example API Route Error");
- return NextResponse.json({ data: "Testing Sentry Error..." });
-}
diff --git a/src/app/api/timer/[id]/route.ts b/src/app/api/timer/[id]/route.ts
index 40f08e9..12401e3 100644
--- a/src/app/api/timer/[id]/route.ts
+++ b/src/app/api/timer/[id]/route.ts
@@ -1,18 +1,14 @@
-import { updateTimerTime } from "~/server/queries";
+import { updateTimerTime } from "@/server/queries";
export async function POST(
_: Request,
{ params }: { params: Promise<{ id: string }> },
) {
- const idAsNumber = parseInt((await params).id);
-
try {
- const updated = await updateTimerTime(idAsNumber);
+ await updateTimerTime((await params).id);
return Response.json({}, { status: 200 });
} catch (error: any) {
- const typedError = error as Error;
-
if (error.message === "Timer not found") {
return Response.json({}, { status: 404 });
} else {
diff --git a/src/app/timer/[id]/page.tsx b/src/app/timer/[id]/page.tsx
index 9fe0deb..1760422 100644
--- a/src/app/timer/[id]/page.tsx
+++ b/src/app/timer/[id]/page.tsx
@@ -1,5 +1,5 @@
-import FullPageTimer from "~/app/_components/full-page-timer";
-import { getTimer } from "~/server/queries";
+import FullPageTimer from "@/app/_components/full-page-timer";
+import { getTimer } from "@/server/queries";
export default async function TimerPage({
params,
diff --git a/src/app/timer/create/create-timer.tsx b/src/app/timer/create/create-timer.tsx
new file mode 100644
index 0000000..ecb27e8
--- /dev/null
+++ b/src/app/timer/create/create-timer.tsx
@@ -0,0 +1,7 @@
+"use server";
+
+import { createTimer } from "@/server/queries";
+
+export async function handleCreateTimer(caption: string, isPublic: boolean) {
+ return await createTimer(caption, isPublic);
+}
diff --git a/src/app/timer/create/page.tsx b/src/app/timer/create/page.tsx
new file mode 100644
index 0000000..bd9e69c
--- /dev/null
+++ b/src/app/timer/create/page.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { handleCreateTimer } from "./create-timer";
+
+const formSchema = z.object({
+ caption: z.string().min(2, {
+ message: "Username must be at least 2 characters.",
+ }),
+ public: z.boolean(),
+});
+
+export default function ProfileForm() {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ caption: "",
+ public: false,
+ },
+ });
+
+ async function onSubmit(values: z.infer) {
+ await handleCreateTimer(values.caption, values.public);
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/timer/page.tsx b/src/app/timer/page.tsx
new file mode 100644
index 0000000..75a1c75
--- /dev/null
+++ b/src/app/timer/page.tsx
@@ -0,0 +1,48 @@
+import { createHash } from "crypto";
+import Link from "next/link";
+import Timer from "~/app/_components/timer";
+import { getAllMyTimers } from "~/server/queries";
+
+const hashEmail = (email: string) => {
+ const hash = createHash("sha256")
+ .update(email.trim().toLowerCase())
+ .digest("hex");
+
+ return hash;
+};
+
+export default async function HomePage() {
+ const timers = await getAllMyTimers();
+
+ const modulo = timers.length > 4 ? Math.floor(Math.random() * 4) + 2 : 1;
+
+ return (
+
+ {timers.map((timer, idx) =>
+ idx % modulo == 0 ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ ),
+ )}
+
+ );
+}
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
index 05e961c..09cd14d 100644
--- a/src/components/ui/avatar.tsx
+++ b/src/components/ui/avatar.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
-import { cn } from "~/lib/utils"
+import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
- className
+ className,
)}
{...props}
/>
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
- className
+ className,
)}
{...props}
/>
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
-export { Avatar, AvatarImage, AvatarFallback }
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..d09a695
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index 874ce09..1cc0c12 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "~/lib/utils"
+import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
- className
+ className,
)}
{...props}
/>
-))
-Card.displayName = "Card"
+));
+Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
-))
-CardHeader.displayName = "CardHeader"
+));
+CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
-))
-CardTitle.displayName = "CardTitle"
+));
+CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-CardDescription.displayName = "CardDescription"
+));
+CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-))
-CardContent.displayName = "CardContent"
+));
+CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
-))
-CardFooter.displayName = "CardFooter"
+));
+CardFooter.displayName = "CardFooter";
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..07680df
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "~/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index a75e1df..9e25b54 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -1,18 +1,18 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { X } from "lucide-react"
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
-import { cn } from "~/lib/utils"
+import { cn } from "@/lib/utils";
-const Dialog = DialogPrimitive.Root
+const Dialog = DialogPrimitive.Root;
-const DialogTrigger = DialogPrimitive.Trigger
+const DialogTrigger = DialogPrimitive.Trigger;
-const DialogPortal = DialogPrimitive.Portal
+const DialogPortal = DialogPrimitive.Portal;
-const DialogClose = DialogPrimitive.Close
+const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef,
@@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef<
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
@@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
- className
+ className,
)}
{...props}
>
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -60,12 +60,12 @@ const DialogHeader = ({
-)
-DialogHeader.displayName = "DialogHeader"
+);
+DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -74,12 +74,12 @@ const DialogFooter = ({
-)
-DialogFooter.displayName = "DialogFooter"
+);
+DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef,
@@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
- className
+ className,
)}
{...props}
/>
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef,
@@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -119,4 +119,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
-}
+};
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..c3daa38
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form";
+
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue,
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ");
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue,
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = "FormItem";
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = "FormLabel";
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = "FormControl";
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = "FormDescription";
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = "FormMessage";
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+};
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..7db5241
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..84f8b0c
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx
index 3dea8df..8dc3798 100644
--- a/src/components/ui/navigation-menu.tsx
+++ b/src/components/ui/navigation-menu.tsx
@@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
-import { cn } from "~/lib/utils";
+import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef,
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..3bd8058
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "~/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a5ef193..bd0c391 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
+ return twMerge(clsx(inputs))
}
diff --git a/src/server/queries.ts b/src/server/queries.ts
index 0dca67a..67f4557 100644
--- a/src/server/queries.ts
+++ b/src/server/queries.ts
@@ -1,33 +1,77 @@
import "server-only";
-import { eq } from "drizzle-orm";
+import { eq, or } from "drizzle-orm";
import { db } from "./db";
import { timers } from "./db/schema";
-import { notFound } from "next/navigation";
+import { notFound, redirect } from "next/navigation";
+import { auth, currentUser } from "@clerk/nextjs/server";
+
+type NewTimer = typeof timers.$inferInsert;
export async function getTimer(id: string) {
+ const user = await auth();
+
const timer = await db.query.timers.findFirst({
- where: (model, { and, eq }) =>
- and(eq(model.id, id), eq(model.public, true)),
+ where: (model, { and, eq, or }) =>
+ and(
+ eq(model.id, id),
+ or(eq(model.public, true), eq(model.userId, user.userId ?? "")),
+ ),
});
if (!timer) notFound();
- if (!timer.public) notFound();
-
return timer;
}
export async function getAllTimers() {
+ const user = await auth();
+
const timers = db.query.timers
.findMany({
- where: (model, { eq }) => eq(model.public, true),
+ where: (model) =>
+ or(eq(model.public, true), eq(model.userId, user.userId ?? "")),
})
.execute();
return timers;
}
+export async function getAllMyTimers() {
+ const user = await auth();
+ if (!user.userId) throw new Error("Not authenticated");
+
+ const timers = db.query.timers
+ .findMany({
+ where: (model, { eq }) => eq(model.userId, user.userId),
+ })
+ .execute();
+
+ return timers;
+}
+
+export async function createTimer(caption: string, isPublic: boolean) {
+ const user = await auth();
+ if (!user.userId) throw new Error("Not authenticated");
+
+ const userInfo = await currentUser();
+
+ const insertTimer = (timer: NewTimer) => {
+ return db.insert(timers).values(timer).returning({ id: timers.id });
+ };
+
+ const [timer] = await insertTimer({
+ userId: user.userId,
+ userEmail: userInfo?.emailAddresses[0]?.emailAddress ?? "",
+ caption,
+ public: isPublic,
+ });
+
+ if (!timer) throw new Error("Timer not created");
+
+ redirect(`/timer/${timer.id}`);
+}
+
export async function updateTimerTime(id: string) {
const updated = await db
.update(timers)
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 75f6040..2b4432a 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -4,57 +4,55 @@
@layer base {
:root {
--background: 0 0% 100%;
- --foreground: 240 10% 3.9%;
+ --foreground: 0 0% 3.9%;
--card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
+ --card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 10% 3.9%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
- --radius: 0.5rem
- }
+ --radius: 0.5rem}
.dark {
- --background: 240 10% 3.9%;
+ --background: 0 0% 3.9%;
--foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
+ --card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
+ --popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%
- }
+ --chart-5: 340 75% 55%}
}
@layer base {
* {
diff --git a/tsconfig.json b/tsconfig.json
index 905062d..f8915c8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,6 +26,7 @@
/* Path Aliases */
"baseUrl": ".",
"paths": {
+ "@/*": ["./src/*"],
"~/*": ["./src/*"]
}
},