@@ -280,6 +361,60 @@ export default function Settings({
+ {atprotoSignedIn ? (
+ <>
+
+
+
+ setDsAddress(e.target.value)}
+ />
+
+
+
+ This writes app.onecalendar.ds for your Bluesky DID.
+
+
+
+
+
+
+ setTargetDsAddress(e.target.value)}
+ />
+
+
+ {dsMigrationProgress ? (
+
+ {dsMigrationProgress}
+
+ ) : null}
+ {dsMessage ? (
+
{dsMessage}
+ ) : null}
+
+ >
+ ) : isSignedIn ? (
+
+ Custom DS is available for ATProto accounts only.
+
+ ) : null}
+
{
- fetch("/api/atproto/session")
+ fetch("/api/atproto/session", { cache: "no-store" })
.then((r) => r.json())
.then(
(data: {
diff --git a/components/app/sidebar/bookmark-panel.tsx b/apps/web/components/app/sidebar/bookmark-panel.tsx
similarity index 100%
rename from components/app/sidebar/bookmark-panel.tsx
rename to apps/web/components/app/sidebar/bookmark-panel.tsx
diff --git a/components/app/sidebar/countdown.tsx b/apps/web/components/app/sidebar/countdown.tsx
similarity index 100%
rename from components/app/sidebar/countdown.tsx
rename to apps/web/components/app/sidebar/countdown.tsx
diff --git a/components/app/sidebar/mini-calendar-sheet.tsx b/apps/web/components/app/sidebar/mini-calendar-sheet.tsx
similarity index 100%
rename from components/app/sidebar/mini-calendar-sheet.tsx
rename to apps/web/components/app/sidebar/mini-calendar-sheet.tsx
diff --git a/components/app/sidebar/right-sidebar.tsx b/apps/web/components/app/sidebar/right-sidebar.tsx
similarity index 100%
rename from components/app/sidebar/right-sidebar.tsx
rename to apps/web/components/app/sidebar/right-sidebar.tsx
diff --git a/components/app/sidebar/sidebar.tsx b/apps/web/components/app/sidebar/sidebar.tsx
similarity index 100%
rename from components/app/sidebar/sidebar.tsx
rename to apps/web/components/app/sidebar/sidebar.tsx
diff --git a/components/app/views/day-view.tsx b/apps/web/components/app/views/day-view.tsx
similarity index 100%
rename from components/app/views/day-view.tsx
rename to apps/web/components/app/views/day-view.tsx
diff --git a/components/app/views/month-view.tsx b/apps/web/components/app/views/month-view.tsx
similarity index 100%
rename from components/app/views/month-view.tsx
rename to apps/web/components/app/views/month-view.tsx
diff --git a/components/app/views/week-view.tsx b/apps/web/components/app/views/week-view.tsx
similarity index 100%
rename from components/app/views/week-view.tsx
rename to apps/web/components/app/views/week-view.tsx
diff --git a/components/app/views/year-view.tsx b/apps/web/components/app/views/year-view.tsx
similarity index 100%
rename from components/app/views/year-view.tsx
rename to apps/web/components/app/views/year-view.tsx
diff --git a/components/auth/atproto-login-form.tsx b/apps/web/components/auth/atproto-login-form.tsx
similarity index 100%
rename from components/auth/atproto-login-form.tsx
rename to apps/web/components/auth/atproto-login-form.tsx
diff --git a/components/auth/auth-brand.tsx b/apps/web/components/auth/auth-brand.tsx
similarity index 100%
rename from components/auth/auth-brand.tsx
rename to apps/web/components/auth/auth-brand.tsx
diff --git a/components/auth/login-form.tsx b/apps/web/components/auth/login-form.tsx
similarity index 100%
rename from components/auth/login-form.tsx
rename to apps/web/components/auth/login-form.tsx
diff --git a/components/auth/reset-form.tsx b/apps/web/components/auth/reset-form.tsx
similarity index 100%
rename from components/auth/reset-form.tsx
rename to apps/web/components/auth/reset-form.tsx
diff --git a/components/auth/sign-up-form.tsx b/apps/web/components/auth/sign-up-form.tsx
similarity index 100%
rename from components/auth/sign-up-form.tsx
rename to apps/web/components/auth/sign-up-form.tsx
diff --git a/components/icons/clock-dashed.tsx b/apps/web/components/icons/clock-dashed.tsx
similarity index 100%
rename from components/icons/clock-dashed.tsx
rename to apps/web/components/icons/clock-dashed.tsx
diff --git a/components/icons/share.tsx b/apps/web/components/icons/share.tsx
similarity index 100%
rename from components/icons/share.tsx
rename to apps/web/components/icons/share.tsx
diff --git a/components/landing/animated-sphere.tsx b/apps/web/components/landing/animated-sphere.tsx
similarity index 100%
rename from components/landing/animated-sphere.tsx
rename to apps/web/components/landing/animated-sphere.tsx
diff --git a/components/landing/animated-tetrahedron.tsx b/apps/web/components/landing/animated-tetrahedron.tsx
similarity index 100%
rename from components/landing/animated-tetrahedron.tsx
rename to apps/web/components/landing/animated-tetrahedron.tsx
diff --git a/components/landing/animated-wave.tsx b/apps/web/components/landing/animated-wave.tsx
similarity index 100%
rename from components/landing/animated-wave.tsx
rename to apps/web/components/landing/animated-wave.tsx
diff --git a/components/landing/cta-section.tsx b/apps/web/components/landing/cta-section.tsx
similarity index 100%
rename from components/landing/cta-section.tsx
rename to apps/web/components/landing/cta-section.tsx
diff --git a/components/landing/developers-section.tsx b/apps/web/components/landing/developers-section.tsx
similarity index 100%
rename from components/landing/developers-section.tsx
rename to apps/web/components/landing/developers-section.tsx
diff --git a/components/landing/features-section.tsx b/apps/web/components/landing/features-section.tsx
similarity index 100%
rename from components/landing/features-section.tsx
rename to apps/web/components/landing/features-section.tsx
diff --git a/components/landing/footer-section.tsx b/apps/web/components/landing/footer-section.tsx
similarity index 100%
rename from components/landing/footer-section.tsx
rename to apps/web/components/landing/footer-section.tsx
diff --git a/components/landing/hero-section.tsx b/apps/web/components/landing/hero-section.tsx
similarity index 100%
rename from components/landing/hero-section.tsx
rename to apps/web/components/landing/hero-section.tsx
diff --git a/components/landing/how-it-works-section.tsx b/apps/web/components/landing/how-it-works-section.tsx
similarity index 100%
rename from components/landing/how-it-works-section.tsx
rename to apps/web/components/landing/how-it-works-section.tsx
diff --git a/components/landing/index.ts b/apps/web/components/landing/index.ts
similarity index 100%
rename from components/landing/index.ts
rename to apps/web/components/landing/index.ts
diff --git a/components/landing/infrastructure-section.tsx b/apps/web/components/landing/infrastructure-section.tsx
similarity index 100%
rename from components/landing/infrastructure-section.tsx
rename to apps/web/components/landing/infrastructure-section.tsx
diff --git a/components/landing/integrations-section.tsx b/apps/web/components/landing/integrations-section.tsx
similarity index 100%
rename from components/landing/integrations-section.tsx
rename to apps/web/components/landing/integrations-section.tsx
diff --git a/components/landing/metrics-section.tsx b/apps/web/components/landing/metrics-section.tsx
similarity index 100%
rename from components/landing/metrics-section.tsx
rename to apps/web/components/landing/metrics-section.tsx
diff --git a/components/landing/navigation.tsx b/apps/web/components/landing/navigation.tsx
similarity index 100%
rename from components/landing/navigation.tsx
rename to apps/web/components/landing/navigation.tsx
diff --git a/components/landing/pricing-section.tsx b/apps/web/components/landing/pricing-section.tsx
similarity index 100%
rename from components/landing/pricing-section.tsx
rename to apps/web/components/landing/pricing-section.tsx
diff --git a/components/landing/testimonials-section.tsx b/apps/web/components/landing/testimonials-section.tsx
similarity index 100%
rename from components/landing/testimonials-section.tsx
rename to apps/web/components/landing/testimonials-section.tsx
diff --git a/components/providers/calendar-context.tsx b/apps/web/components/providers/calendar-context.tsx
similarity index 100%
rename from components/providers/calendar-context.tsx
rename to apps/web/components/providers/calendar-context.tsx
diff --git a/components/providers/pwa-provider.tsx b/apps/web/components/providers/pwa-provider.tsx
similarity index 100%
rename from components/providers/pwa-provider.tsx
rename to apps/web/components/providers/pwa-provider.tsx
diff --git a/components/providers/theme-provider.tsx b/apps/web/components/providers/theme-provider.tsx
similarity index 100%
rename from components/providers/theme-provider.tsx
rename to apps/web/components/providers/theme-provider.tsx
diff --git a/hooks/use-toast.ts b/apps/web/hooks/use-toast.ts
similarity index 100%
rename from hooks/use-toast.ts
rename to apps/web/hooks/use-toast.ts
diff --git a/hooks/useLocalStorage.ts b/apps/web/hooks/useLocalStorage.ts
similarity index 100%
rename from hooks/useLocalStorage.ts
rename to apps/web/hooks/useLocalStorage.ts
diff --git a/hooks/useMobile.ts b/apps/web/hooks/useMobile.ts
similarity index 100%
rename from hooks/useMobile.ts
rename to apps/web/hooks/useMobile.ts
diff --git a/lib/atproto-auth.ts b/apps/web/lib/atproto-auth.ts
similarity index 91%
rename from lib/atproto-auth.ts
rename to apps/web/lib/atproto-auth.ts
index bc06cfa6..d9b3e190 100644
--- a/lib/atproto-auth.ts
+++ b/apps/web/lib/atproto-auth.ts
@@ -17,6 +17,19 @@ export interface AtprotoSession {
dpopPublicJwk?: DpopPublicJwk;
}
+function sanitizeSessionForCookie(session: AtprotoSession): AtprotoSession {
+ return {
+ did: session.did,
+ handle: session.handle,
+ pds: session.pds,
+ accessToken: session.accessToken,
+ dpopPrivateKeyPem: session.dpopPrivateKeyPem,
+ dpopPublicJwk: session.dpopPublicJwk,
+ displayName: session.displayName,
+ avatar: session.avatar,
+ };
+}
+
export interface KeyEntry {
kid: string;
secret: string;
@@ -133,7 +146,7 @@ export async function setAtprotoSession(session: AtprotoSession) {
store.delete(ATPROTO_SESSION_COOKIE);
return;
}
- const value = encodeSession(session);
+ const value = encodeSession(sanitizeSessionForCookie(session));
store.set(ATPROTO_SESSION_COOKIE, value, {
httpOnly: true,
secure: shouldUseSecureCookies(),
diff --git a/apps/web/lib/atproto-feature.ts b/apps/web/lib/atproto-feature.ts
new file mode 100644
index 00000000..cb898f7e
--- /dev/null
+++ b/apps/web/lib/atproto-feature.ts
@@ -0,0 +1,12 @@
+import { NextResponse } from "next/server";
+
+export const ATPROTO_DISABLED =
+ process.env.NEXT_PUBLIC_DISABLE_ATPROTO === "1" ||
+ process.env.NEXT_PUBLIC_DISABLE_ATPROTO === "true";
+
+export function atprotoDisabledResponse() {
+ return NextResponse.json(
+ { error: "ATProto channel is disabled" },
+ { status: 410 },
+ );
+}
diff --git a/lib/atproto-oauth-txn.ts b/apps/web/lib/atproto-oauth-txn.ts
similarity index 100%
rename from lib/atproto-oauth-txn.ts
rename to apps/web/lib/atproto-oauth-txn.ts
diff --git a/lib/atproto.ts b/apps/web/lib/atproto.ts
similarity index 100%
rename from lib/atproto.ts
rename to apps/web/lib/atproto.ts
diff --git a/lib/crypto.ts b/apps/web/lib/crypto.ts
similarity index 100%
rename from lib/crypto.ts
rename to apps/web/lib/crypto.ts
diff --git a/lib/dpop.ts b/apps/web/lib/dpop.ts
similarity index 100%
rename from lib/dpop.ts
rename to apps/web/lib/dpop.ts
diff --git a/apps/web/lib/ds-client.ts b/apps/web/lib/ds-client.ts
new file mode 100644
index 00000000..c6b30897
--- /dev/null
+++ b/apps/web/lib/ds-client.ts
@@ -0,0 +1,19 @@
+export async function signedDsRequest(params: {
+ ds: string;
+ path: string;
+ method: "GET" | "POST" | "DELETE";
+ payload?: unknown;
+}) {
+ const res = await fetch("/api/ds/proxy", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ });
+
+ if (!res.ok) {
+ const message = await res.text();
+ throw new Error(message || `DS request failed: ${res.status}`);
+ }
+
+ return (await res.json()) as T;
+}
diff --git a/apps/web/lib/ds-signed-request.ts b/apps/web/lib/ds-signed-request.ts
new file mode 100644
index 00000000..5be79f74
--- /dev/null
+++ b/apps/web/lib/ds-signed-request.ts
@@ -0,0 +1,58 @@
+import { createHash, createSign } from "node:crypto";
+import type { AtprotoSession } from "@/lib/atproto-auth";
+
+function digest(value: string) {
+ return createHash("sha256").update(value, "utf8").digest("hex");
+}
+
+function createPayload(method: string, path: string, timestamp: string, body: string) {
+ return `${method.toUpperCase()}\n${path}\n${timestamp}\n${digest(body)}`;
+}
+
+function signPayload(payload: string, privateKeyPem: string) {
+ const signer = createSign("SHA256");
+ signer.update(payload);
+ signer.end();
+ return signer.sign(privateKeyPem).toString("base64url");
+}
+
+export async function signedDsFetch(params: {
+ session: AtprotoSession;
+ ds: string;
+ path: string;
+ method: "GET" | "POST" | "DELETE";
+ body?: unknown;
+}) {
+ if (!params.session.dpopPrivateKeyPem || !params.session.dpopPublicJwk) {
+ throw new Error("ATProto DPoP key unavailable for signed DS request");
+ }
+
+ const ds = params.ds.replace(/\/$/, "");
+ const path = params.path.startsWith("/") ? params.path : `/${params.path}`;
+ const timestamp = Date.now().toString();
+ const bodyText =
+ params.body === undefined || params.method === "GET"
+ ? ""
+ : JSON.stringify(params.body);
+
+ const payload = createPayload(params.method, path, timestamp, bodyText);
+ const signature = signPayload(payload, params.session.dpopPrivateKeyPem);
+
+ const headers: Record = {
+ "x-did": params.session.did,
+ "x-timestamp": timestamp,
+ "x-signature": signature,
+ "x-dpop-jwk": JSON.stringify(params.session.dpopPublicJwk),
+ };
+
+ if (params.method !== "GET") {
+ headers["Content-Type"] = "application/json";
+ }
+
+ return fetch(`${ds}${path}`, {
+ method: params.method,
+ headers,
+ body: params.method === "GET" ? undefined : bodyText,
+ cache: "no-store",
+ });
+}
diff --git a/lib/fetch-json.ts b/apps/web/lib/fetch-json.ts
similarity index 100%
rename from lib/fetch-json.ts
rename to apps/web/lib/fetch-json.ts
diff --git a/lib/gen-oauth-metadata.mjs b/apps/web/lib/gen-oauth-metadata.mjs
similarity index 100%
rename from lib/gen-oauth-metadata.mjs
rename to apps/web/lib/gen-oauth-metadata.mjs
diff --git a/lib/icsUtils.ts b/apps/web/lib/icsUtils.ts
similarity index 100%
rename from lib/icsUtils.ts
rename to apps/web/lib/icsUtils.ts
diff --git a/lib/notifications.ts b/apps/web/lib/notifications.ts
similarity index 100%
rename from lib/notifications.ts
rename to apps/web/lib/notifications.ts
diff --git a/lib/time-analytics.ts b/apps/web/lib/time-analytics.ts
similarity index 100%
rename from lib/time-analytics.ts
rename to apps/web/lib/time-analytics.ts
diff --git a/lib/utils.ts b/apps/web/lib/utils.ts
similarity index 100%
rename from lib/utils.ts
rename to apps/web/lib/utils.ts
diff --git a/next.config.ts b/apps/web/next.config.ts
similarity index 100%
rename from next.config.ts
rename to apps/web/next.config.ts
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 00000000..5db1654c
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "web",
+ "version": "2.2.7",
+ "private": true,
+ "packageManager": "bun@1.3.8",
+ "scripts": {
+ "dev": "(cd ../../packages/i18n && bun run generate:locales) && bun run generate:oauth-metadata && next dev",
+ "build": "(cd ../../packages/i18n && bun run generate:locales) && bun run generate:oauth-metadata && next build",
+ "start": "next start",
+ "generate:locales": "(cd ../../packages/i18n && bun run generate:locales)",
+ "generate:oauth-metadata": "bun lib/gen-oauth-metadata.mjs"
+ },
+ "dependencies": {
+ "@clerk/localizations": "latest",
+ "@clerk/nextjs": "latest",
+ "@hookform/resolvers": "5.2.2",
+ "@marsidev/react-turnstile": "latest",
+ "@radix-ui/react-accordion": "latest",
+ "@radix-ui/react-alert-dialog": "^1.1.7",
+ "@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-context-menu": "^2.2.4",
+ "@radix-ui/react-dialog": "latest",
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-hover-card": "^1.1.4",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-menubar": "^1.1.4",
+ "@radix-ui/react-navigation-menu": "^1.2.3",
+ "@radix-ui/react-popover": "latest",
+ "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-select": "^2.1.4",
+ "@radix-ui/react-separator": "^1.1.1",
+ "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "latest",
+ "@radix-ui/react-toggle": "^1.1.1",
+ "@radix-ui/react-toggle-group": "^1.1.1",
+ "@radix-ui/react-tooltip": "^1.1.6",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "latest",
+ "date-fns": "4.1.0",
+ "embla-carousel-react": "8.6.0",
+ "framer-motion": "^12.7.4",
+ "geist": "latest",
+ "ics": "latest",
+ "input-otp": "1.4.2",
+ "lucide-react": "0.468.0",
+ "motion": "latest",
+ "next": "16.2.1",
+ "next-themes": "latest",
+ "pg": "latest",
+ "qr-code-styling": "latest",
+ "radix-ui": "latest",
+ "react": "^18",
+ "react-day-picker": "9.14.0",
+ "react-dom": "^18",
+ "react-hook-form": "^7.54.1",
+ "react-markdown": "latest",
+ "react-resizable-panels": "4.7.6",
+ "recharts": "3.8.1",
+ "remark-gfm": "latest",
+ "sonner": "2.0.7",
+ "tailwind-merge": "3.5.0",
+ "tailwindcss-animate": "^1.0.7",
+ "vaul": "1.1.2",
+ "zod": "4.3.6"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "4.2.2",
+ "@types/node": "25.5.0",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "postcss": "8.5.8",
+ "tailwindcss": "4.2.2",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs
new file mode 100644
index 00000000..1ed08401
--- /dev/null
+++ b/apps/web/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from "../../packages/config/postcss.config.mjs";
diff --git a/proxy.ts b/apps/web/proxy.ts
similarity index 100%
rename from proxy.ts
rename to apps/web/proxy.ts
diff --git a/public/Banner-dark.jpg b/apps/web/public/Banner-dark.jpg
similarity index 100%
rename from public/Banner-dark.jpg
rename to apps/web/public/Banner-dark.jpg
diff --git a/public/Banner.jpg b/apps/web/public/Banner.jpg
similarity index 100%
rename from public/Banner.jpg
rename to apps/web/public/Banner.jpg
diff --git a/public/Home.jpg b/apps/web/public/Home.jpg
similarity index 100%
rename from public/Home.jpg
rename to apps/web/public/Home.jpg
diff --git a/public/file.svg b/apps/web/public/file.svg
similarity index 100%
rename from public/file.svg
rename to apps/web/public/file.svg
diff --git a/public/globe.svg b/apps/web/public/globe.svg
similarity index 100%
rename from public/globe.svg
rename to apps/web/public/globe.svg
diff --git a/public/icon.svg b/apps/web/public/icon.svg
similarity index 100%
rename from public/icon.svg
rename to apps/web/public/icon.svg
diff --git a/public/oauth-client-metadata.json b/apps/web/public/oauth-client-metadata.json
similarity index 100%
rename from public/oauth-client-metadata.json
rename to apps/web/public/oauth-client-metadata.json
diff --git a/public/og.png b/apps/web/public/og.png
similarity index 100%
rename from public/og.png
rename to apps/web/public/og.png
diff --git a/public/sf.otf b/apps/web/public/sf.otf
similarity index 100%
rename from public/sf.otf
rename to apps/web/public/sf.otf
diff --git a/public/sw.js b/apps/web/public/sw.js
similarity index 100%
rename from public/sw.js
rename to apps/web/public/sw.js
diff --git a/public/vercel.svg b/apps/web/public/vercel.svg
similarity index 100%
rename from public/vercel.svg
rename to apps/web/public/vercel.svg
diff --git a/public/window.svg b/apps/web/public/window.svg
similarity index 100%
rename from public/window.svg
rename to apps/web/public/window.svg
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
new file mode 100644
index 00000000..8ebd4130
--- /dev/null
+++ b/apps/web/tailwind.config.ts
@@ -0,0 +1 @@
+export { default } from "../../packages/config/tailwind.config";
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 00000000..0cfdcf03
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../packages/config/tsconfig.base.json",
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"],
+ "@/components/ui/*": ["../../packages/ui/src/*"],
+ "@/lib/i18n": ["../../packages/i18n/src/i18n.ts"],
+ "@/lib/locales": ["../../packages/i18n/src/locales.ts"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/vercel.json b/apps/web/vercel.json
similarity index 100%
rename from vercel.json
rename to apps/web/vercel.json
diff --git a/lib/atproto-feature.ts b/lib/atproto-feature.ts
deleted file mode 100644
index b470bede..00000000
--- a/lib/atproto-feature.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NextResponse } from "next/server";
-
-export const ATPROTO_DISABLED = false;
-
-export function atprotoDisabledResponse() {
- return NextResponse.json({ error: "ATProto channel is disabled" }, { status: 410 });
-}
-
-/*
-import { NextResponse } from "next/server";
-
-export const ATPROTO_DISABLED = true;
-
-export function atprotoDisabledResponse() {
- return NextResponse.json({ error: "ATProto channel is disabled" }, { status: 410 });
-}
-*/
diff --git a/package.json b/package.json
index 75fc009e..b1402987 100644
--- a/package.json
+++ b/package.json
@@ -1,81 +1,22 @@
{
- "name": "one-calendar",
- "version": "2.2.7",
+ "name": "one-calendar-monorepo",
"private": true,
"packageManager": "bun@1.3.8",
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ],
"scripts": {
- "dev": "bun run generate:locales && next dev",
- "build": "bun run generate:locales && next build",
- "start": "next start",
- "generate:locales": "bun lib/gen-locales.mjs",
- "generate:oauth-metadata": "bun lib/gen-oauth-metadata.mjs"
- },
- "dependencies": {
- "@clerk/localizations": "latest",
- "@clerk/nextjs": "latest",
- "@hookform/resolvers": "5.2.2",
- "@marsidev/react-turnstile": "latest",
- "@radix-ui/react-accordion": "latest",
- "@radix-ui/react-alert-dialog": "^1.1.7",
- "@radix-ui/react-avatar": "^1.1.2",
- "@radix-ui/react-checkbox": "^1.1.3",
- "@radix-ui/react-collapsible": "^1.1.2",
- "@radix-ui/react-context-menu": "^2.2.4",
- "@radix-ui/react-dialog": "latest",
- "@radix-ui/react-dropdown-menu": "^2.1.4",
- "@radix-ui/react-hover-card": "^1.1.4",
- "@radix-ui/react-label": "^2.1.1",
- "@radix-ui/react-menubar": "^1.1.4",
- "@radix-ui/react-navigation-menu": "^1.2.3",
- "@radix-ui/react-popover": "latest",
- "@radix-ui/react-scroll-area": "^1.2.2",
- "@radix-ui/react-select": "^2.1.4",
- "@radix-ui/react-separator": "^1.1.1",
- "@radix-ui/react-slider": "^1.2.2",
- "@radix-ui/react-slot": "^1.2.0",
- "@radix-ui/react-switch": "^1.1.2",
- "@radix-ui/react-tabs": "^1.1.2",
- "@radix-ui/react-toast": "latest",
- "@radix-ui/react-toggle": "^1.1.1",
- "@radix-ui/react-toggle-group": "^1.1.1",
- "@radix-ui/react-tooltip": "^1.1.6",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cmdk": "latest",
- "date-fns": "4.1.0",
- "embla-carousel-react": "8.6.0",
- "framer-motion": "^12.7.4",
- "geist": "latest",
- "ics": "latest",
- "input-otp": "1.4.2",
- "lucide-react": "latest",
- "motion": "latest",
- "next": "16.2.1",
- "next-themes": "latest",
- "pg": "latest",
- "qr-code-styling": "latest",
- "radix-ui": "latest",
- "react": "^18",
- "react-day-picker": "9.14.0",
- "react-dom": "^18",
- "react-hook-form": "^7.54.1",
- "react-markdown": "latest",
- "react-resizable-panels": "^2.1.7",
- "recharts": "3.7.0",
- "remark-gfm": "latest",
- "sonner": "2.0.7",
- "tailwind-merge": "3.5.0",
- "tailwindcss-animate": "^1.0.7",
- "vaul": "1.1.2",
- "zod": "4.3.6"
+ "dev": "turbo dev --filter=web",
+ "build": "turbo build",
+ "start": "turbo start --filter=web",
+ "generate:locales": "turbo generate:locales --filter=@repo/i18n",
+ "generate:oauth-metadata": "turbo generate:oauth-metadata --filter=web",
+ "dev:web": "turbo dev --filter=web",
+ "dev:ds": "turbo dev --filter=ds",
+ "start:ds": "turbo start --filter=ds"
},
"devDependencies": {
- "@tailwindcss/postcss": "4.2.1",
- "@types/node": "25.2.3",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "postcss": "8.5.6",
- "tailwindcss": "4.2.1",
- "typescript": "5.9.3"
+ "turbo": "^2.5.8"
}
}
diff --git a/packages/config/package.json b/packages/config/package.json
new file mode 100644
index 00000000..549604e5
--- /dev/null
+++ b/packages/config/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@repo/config",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./postcss": "./postcss.config.mjs",
+ "./tailwind": "./tailwind.config.ts",
+ "./tsconfig": "./tsconfig.base.json"
+ }
+}
diff --git a/postcss.config.mjs b/packages/config/postcss.config.mjs
similarity index 100%
rename from postcss.config.mjs
rename to packages/config/postcss.config.mjs
diff --git a/packages/config/tailwind.config.ts b/packages/config/tailwind.config.ts
new file mode 100644
index 00000000..20147bb6
--- /dev/null
+++ b/packages/config/tailwind.config.ts
@@ -0,0 +1,5 @@
+import type { Config } from "tailwindcss";
+
+const config = {} satisfies Config;
+
+export default config;
diff --git a/tsconfig.json b/packages/config/tsconfig.base.json
similarity index 56%
rename from tsconfig.json
rename to packages/config/tsconfig.base.json
index a5575e9d..9703a038 100644
--- a/tsconfig.json
+++ b/packages/config/tsconfig.base.json
@@ -12,16 +12,6 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "incremental": true
+ }
}
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
new file mode 100644
index 00000000..0e7db478
--- /dev/null
+++ b/packages/i18n/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@repo/i18n",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "generate:locales": "bun scripts/gen-locales.mjs"
+ }
+}
diff --git a/lib/gen-locales.mjs b/packages/i18n/scripts/gen-locales.mjs
similarity index 74%
rename from lib/gen-locales.mjs
rename to packages/i18n/scripts/gen-locales.mjs
index 9c526f07..39cdc06c 100644
--- a/lib/gen-locales.mjs
+++ b/packages/i18n/scripts/gen-locales.mjs
@@ -5,8 +5,8 @@ import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const projectRoot = path.resolve(__dirname, "..")
-const localesDir = path.join(projectRoot, "locales")
-const outputFile = path.join(projectRoot, "lib", "locales.ts")
+const localesDir = path.join(projectRoot, "src", "locales")
+const outputFile = path.join(projectRoot, "src", "locales.ts")
const toIdentifier = (value) =>
`locale${value.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()).replace(/^[a-z]/, (chr) => chr.toUpperCase())}`
@@ -17,14 +17,14 @@ const run = async () => {
.sort((a, b) => a.localeCompare(b))
if (localeFiles.length === 0) {
- throw new Error("No locale json files found in /locales")
+ throw new Error("No locale json files found in /src/locales")
}
const imports = localeFiles
.map((file) => {
const lang = file.replace(/\.json$/, "")
const identifier = toIdentifier(lang)
- return `import ${identifier} from "@/locales/${file}"`
+ return `import ${identifier} from "./locales/${file}"`
})
.join("\n")
@@ -36,7 +36,7 @@ const run = async () => {
})
.join("\n")
- const content = `/* eslint-disable */\n// This file is auto-generated by lib/gen-locales.mjs\n// Do not edit manually.\n\n${imports}\n\nexport const translations = {\n${entries}\n} as const\n\nexport type Language = keyof typeof translations\n`
+ const content = `/* eslint-disable */\n// This file is auto-generated by packages/i18n/scripts/gen-locales.mjs\n// Do not edit manually.\n\n${imports}\n\nexport const translations = {\n${entries}\n} as const\n\nexport type Language = keyof typeof translations\n`
await fs.writeFile(outputFile, content, "utf8")
console.log(`Generated ${path.relative(projectRoot, outputFile)} with ${localeFiles.length} locale(s).`)
diff --git a/lib/i18n.ts b/packages/i18n/src/i18n.ts
similarity index 99%
rename from lib/i18n.ts
rename to packages/i18n/src/i18n.ts
index e1233873..79657d94 100644
--- a/lib/i18n.ts
+++ b/packages/i18n/src/i18n.ts
@@ -10,7 +10,7 @@ import {
import {
translations as localeTranslations,
type Language,
-} from "@/lib/locales";
+} from "./locales";
const LANGUAGE_STORAGE_KEY = "preferred-language";
diff --git a/locales/bn.json b/packages/i18n/src/locales/bn.json
similarity index 100%
rename from locales/bn.json
rename to packages/i18n/src/locales/bn.json
diff --git a/locales/de.json b/packages/i18n/src/locales/de.json
similarity index 100%
rename from locales/de.json
rename to packages/i18n/src/locales/de.json
diff --git a/locales/el.json b/packages/i18n/src/locales/el.json
similarity index 100%
rename from locales/el.json
rename to packages/i18n/src/locales/el.json
diff --git a/locales/en-GB.json b/packages/i18n/src/locales/en-GB.json
similarity index 100%
rename from locales/en-GB.json
rename to packages/i18n/src/locales/en-GB.json
diff --git a/locales/en.json b/packages/i18n/src/locales/en.json
similarity index 100%
rename from locales/en.json
rename to packages/i18n/src/locales/en.json
diff --git a/locales/es.json b/packages/i18n/src/locales/es.json
similarity index 100%
rename from locales/es.json
rename to packages/i18n/src/locales/es.json
diff --git a/locales/fi.json b/packages/i18n/src/locales/fi.json
similarity index 100%
rename from locales/fi.json
rename to packages/i18n/src/locales/fi.json
diff --git a/locales/fr.json b/packages/i18n/src/locales/fr.json
similarity index 100%
rename from locales/fr.json
rename to packages/i18n/src/locales/fr.json
diff --git a/locales/hi.json b/packages/i18n/src/locales/hi.json
similarity index 100%
rename from locales/hi.json
rename to packages/i18n/src/locales/hi.json
diff --git a/locales/is.json b/packages/i18n/src/locales/is.json
similarity index 100%
rename from locales/is.json
rename to packages/i18n/src/locales/is.json
diff --git a/locales/it.json b/packages/i18n/src/locales/it.json
similarity index 100%
rename from locales/it.json
rename to packages/i18n/src/locales/it.json
diff --git a/locales/ja.json b/packages/i18n/src/locales/ja.json
similarity index 100%
rename from locales/ja.json
rename to packages/i18n/src/locales/ja.json
diff --git a/locales/ko.json b/packages/i18n/src/locales/ko.json
similarity index 100%
rename from locales/ko.json
rename to packages/i18n/src/locales/ko.json
diff --git a/locales/lt.json b/packages/i18n/src/locales/lt.json
similarity index 100%
rename from locales/lt.json
rename to packages/i18n/src/locales/lt.json
diff --git a/locales/lv.json b/packages/i18n/src/locales/lv.json
similarity index 100%
rename from locales/lv.json
rename to packages/i18n/src/locales/lv.json
diff --git a/locales/mk.json b/packages/i18n/src/locales/mk.json
similarity index 100%
rename from locales/mk.json
rename to packages/i18n/src/locales/mk.json
diff --git a/locales/nb.json b/packages/i18n/src/locales/nb.json
similarity index 100%
rename from locales/nb.json
rename to packages/i18n/src/locales/nb.json
diff --git a/locales/nl.json b/packages/i18n/src/locales/nl.json
similarity index 100%
rename from locales/nl.json
rename to packages/i18n/src/locales/nl.json
diff --git a/locales/pl.json b/packages/i18n/src/locales/pl.json
similarity index 100%
rename from locales/pl.json
rename to packages/i18n/src/locales/pl.json
diff --git a/locales/pt.json b/packages/i18n/src/locales/pt.json
similarity index 100%
rename from locales/pt.json
rename to packages/i18n/src/locales/pt.json
diff --git a/locales/ro.json b/packages/i18n/src/locales/ro.json
similarity index 100%
rename from locales/ro.json
rename to packages/i18n/src/locales/ro.json
diff --git a/locales/ru.json b/packages/i18n/src/locales/ru.json
similarity index 100%
rename from locales/ru.json
rename to packages/i18n/src/locales/ru.json
diff --git a/locales/sl.json b/packages/i18n/src/locales/sl.json
similarity index 100%
rename from locales/sl.json
rename to packages/i18n/src/locales/sl.json
diff --git a/locales/sq.json b/packages/i18n/src/locales/sq.json
similarity index 100%
rename from locales/sq.json
rename to packages/i18n/src/locales/sq.json
diff --git a/locales/sr.json b/packages/i18n/src/locales/sr.json
similarity index 100%
rename from locales/sr.json
rename to packages/i18n/src/locales/sr.json
diff --git a/locales/sv.json b/packages/i18n/src/locales/sv.json
similarity index 100%
rename from locales/sv.json
rename to packages/i18n/src/locales/sv.json
diff --git a/locales/sw.json b/packages/i18n/src/locales/sw.json
similarity index 100%
rename from locales/sw.json
rename to packages/i18n/src/locales/sw.json
diff --git a/locales/th.json b/packages/i18n/src/locales/th.json
similarity index 100%
rename from locales/th.json
rename to packages/i18n/src/locales/th.json
diff --git a/locales/tr.json b/packages/i18n/src/locales/tr.json
similarity index 100%
rename from locales/tr.json
rename to packages/i18n/src/locales/tr.json
diff --git a/locales/uk.json b/packages/i18n/src/locales/uk.json
similarity index 100%
rename from locales/uk.json
rename to packages/i18n/src/locales/uk.json
diff --git a/locales/vi.json b/packages/i18n/src/locales/vi.json
similarity index 100%
rename from locales/vi.json
rename to packages/i18n/src/locales/vi.json
diff --git a/locales/yue.json b/packages/i18n/src/locales/yue.json
similarity index 100%
rename from locales/yue.json
rename to packages/i18n/src/locales/yue.json
diff --git a/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json
similarity index 100%
rename from locales/zh-CN.json
rename to packages/i18n/src/locales/zh-CN.json
diff --git a/locales/zh-HK.json b/packages/i18n/src/locales/zh-HK.json
similarity index 100%
rename from locales/zh-HK.json
rename to packages/i18n/src/locales/zh-HK.json
diff --git a/locales/zh-TW.json b/packages/i18n/src/locales/zh-TW.json
similarity index 100%
rename from locales/zh-TW.json
rename to packages/i18n/src/locales/zh-TW.json
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 00000000..b8182eeb
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@repo/ui",
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "@radix-ui/react-accordion": "latest",
+ "@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-context-menu": "^2.2.4",
+ "@radix-ui/react-dialog": "latest",
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-separator": "^1.1.1",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "latest",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "0.468.0",
+ "next-themes": "latest",
+ "radix-ui": "latest",
+ "react": "^18",
+ "react-day-picker": "9.14.0",
+ "sonner": "2.0.7",
+ "tailwind-merge": "3.5.0"
+ }
+}
diff --git a/components/ui/accordion.tsx b/packages/ui/src/accordion.tsx
similarity index 98%
rename from components/ui/accordion.tsx
rename to packages/ui/src/accordion.tsx
index f0428981..44fb5933 100644
--- a/components/ui/accordion.tsx
+++ b/packages/ui/src/accordion.tsx
@@ -4,7 +4,7 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Accordion({
...props
diff --git a/components/ui/alert-dialog.tsx b/packages/ui/src/alert-dialog.tsx
similarity index 98%
rename from components/ui/alert-dialog.tsx
rename to packages/ui/src/alert-dialog.tsx
index 89631cbb..7a2fa20f 100644
--- a/components/ui/alert-dialog.tsx
+++ b/packages/ui/src/alert-dialog.tsx
@@ -3,8 +3,8 @@
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
+import { cn } from "./utils";
+import { Button } from "./button";
function AlertDialog({
...props
diff --git a/components/ui/alert.tsx b/packages/ui/src/alert.tsx
similarity index 97%
rename from components/ui/alert.tsx
rename to packages/ui/src/alert.tsx
index 0594a902..1a6e6431 100644
--- a/components/ui/alert.tsx
+++ b/packages/ui/src/alert.tsx
@@ -1,7 +1,7 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
diff --git a/components/ui/avatar.tsx b/packages/ui/src/avatar.tsx
similarity index 96%
rename from components/ui/avatar.tsx
rename to packages/ui/src/avatar.tsx
index 1393f509..5f2e9d04 100644
--- a/components/ui/avatar.tsx
+++ b/packages/ui/src/avatar.tsx
@@ -3,7 +3,7 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Avatar({
className,
diff --git a/components/ui/badge.tsx b/packages/ui/src/badge.tsx
similarity index 97%
rename from components/ui/badge.tsx
rename to packages/ui/src/badge.tsx
index c03a5071..5ba4b6a0 100644
--- a/components/ui/badge.tsx
+++ b/packages/ui/src/badge.tsx
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
diff --git a/components/ui/button.tsx b/packages/ui/src/button.tsx
similarity index 98%
rename from components/ui/button.tsx
rename to packages/ui/src/button.tsx
index 7de6f727..c7be0259 100644
--- a/components/ui/button.tsx
+++ b/packages/ui/src/button.tsx
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
diff --git a/components/ui/calendar.tsx b/packages/ui/src/calendar.tsx
similarity index 98%
rename from components/ui/calendar.tsx
rename to packages/ui/src/calendar.tsx
index c36f9e6d..b1324451 100644
--- a/components/ui/calendar.tsx
+++ b/packages/ui/src/calendar.tsx
@@ -8,8 +8,8 @@ import {
type Locale,
} from "react-day-picker";
-import { cn } from "@/lib/utils";
-import { Button, buttonVariants } from "@/components/ui/button";
+import { cn } from "./utils";
+import { Button, buttonVariants } from "./button";
import {
ChevronLeftIcon,
ChevronRightIcon,
diff --git a/components/ui/card.tsx b/packages/ui/src/card.tsx
similarity index 98%
rename from components/ui/card.tsx
rename to packages/ui/src/card.tsx
index 5c5024c8..fbfa008c 100644
--- a/components/ui/card.tsx
+++ b/packages/ui/src/card.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Card({
className,
diff --git a/components/ui/checkbox.tsx b/packages/ui/src/checkbox.tsx
similarity index 97%
rename from components/ui/checkbox.tsx
rename to packages/ui/src/checkbox.tsx
index 16a50b87..b857c40a 100644
--- a/components/ui/checkbox.tsx
+++ b/packages/ui/src/checkbox.tsx
@@ -3,7 +3,7 @@
import { Checkbox as CheckboxPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
import { CheckIcon } from "lucide-react";
function Checkbox({
diff --git a/components/ui/collapsible.tsx b/packages/ui/src/collapsible.tsx
similarity index 100%
rename from components/ui/collapsible.tsx
rename to packages/ui/src/collapsible.tsx
diff --git a/components/ui/context-menu.tsx b/packages/ui/src/context-menu.tsx
similarity index 99%
rename from components/ui/context-menu.tsx
rename to packages/ui/src/context-menu.tsx
index d0328d60..b8e1132f 100644
--- a/components/ui/context-menu.tsx
+++ b/packages/ui/src/context-menu.tsx
@@ -4,7 +4,7 @@ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const ContextMenu = ContextMenuPrimitive.Root;
diff --git a/components/ui/dialog.tsx b/packages/ui/src/dialog.tsx
similarity index 98%
rename from components/ui/dialog.tsx
rename to packages/ui/src/dialog.tsx
index e480166a..85e5ebb9 100644
--- a/components/ui/dialog.tsx
+++ b/packages/ui/src/dialog.tsx
@@ -3,8 +3,8 @@
import { Dialog as DialogPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
+import { cn } from "./utils";
+import { Button } from "./button";
import { XIcon } from "lucide-react";
function Dialog({
diff --git a/components/ui/dropdown-menu.tsx b/packages/ui/src/dropdown-menu.tsx
similarity index 99%
rename from components/ui/dropdown-menu.tsx
rename to packages/ui/src/dropdown-menu.tsx
index 0fa61745..ae86f6d0 100644
--- a/components/ui/dropdown-menu.tsx
+++ b/packages/ui/src/dropdown-menu.tsx
@@ -4,7 +4,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function DropdownMenu({
...props
diff --git a/components/ui/empty.tsx b/packages/ui/src/empty.tsx
similarity index 98%
rename from components/ui/empty.tsx
rename to packages/ui/src/empty.tsx
index 9f4d3e3d..e03dbdf4 100644
--- a/components/ui/empty.tsx
+++ b/packages/ui/src/empty.tsx
@@ -1,7 +1,7 @@
import type React from "react";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/components/ui/input.tsx b/packages/ui/src/input.tsx
similarity index 96%
rename from components/ui/input.tsx
rename to packages/ui/src/input.tsx
index bcc9491a..5e9a3c7c 100644
--- a/components/ui/input.tsx
+++ b/packages/ui/src/input.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
diff --git a/components/ui/kbd.tsx b/packages/ui/src/kbd.tsx
similarity index 96%
rename from components/ui/kbd.tsx
rename to packages/ui/src/kbd.tsx
index 44fefd02..a3ee0d18 100644
--- a/components/ui/kbd.tsx
+++ b/packages/ui/src/kbd.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
diff --git a/components/ui/label.tsx b/packages/ui/src/label.tsx
similarity index 95%
rename from components/ui/label.tsx
rename to packages/ui/src/label.tsx
index e96d48e3..1c1b152f 100644
--- a/components/ui/label.tsx
+++ b/packages/ui/src/label.tsx
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
diff --git a/components/ui/popover.tsx b/packages/ui/src/popover.tsx
similarity index 98%
rename from components/ui/popover.tsx
rename to packages/ui/src/popover.tsx
index 81bf824f..055af742 100644
--- a/components/ui/popover.tsx
+++ b/packages/ui/src/popover.tsx
@@ -3,7 +3,7 @@
import { Popover as PopoverPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Popover({
...props
diff --git a/components/ui/scroll-area.tsx b/packages/ui/src/scroll-area.tsx
similarity index 98%
rename from components/ui/scroll-area.tsx
rename to packages/ui/src/scroll-area.tsx
index 70a001cc..db7f5790 100644
--- a/components/ui/scroll-area.tsx
+++ b/packages/ui/src/scroll-area.tsx
@@ -3,7 +3,7 @@
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function ScrollArea({
className,
diff --git a/components/ui/select.tsx b/packages/ui/src/select.tsx
similarity index 99%
rename from components/ui/select.tsx
rename to packages/ui/src/select.tsx
index 1c0af238..bcf7e4b3 100644
--- a/components/ui/select.tsx
+++ b/packages/ui/src/select.tsx
@@ -3,7 +3,7 @@
import { Select as SelectPrimitive } from "radix-ui";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
function Select({
diff --git a/components/ui/separator.tsx b/packages/ui/src/separator.tsx
similarity index 95%
rename from components/ui/separator.tsx
rename to packages/ui/src/separator.tsx
index ee783606..d56c4219 100644
--- a/components/ui/separator.tsx
+++ b/packages/ui/src/separator.tsx
@@ -3,7 +3,7 @@
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Separator({
className,
diff --git a/components/ui/sheet.tsx b/packages/ui/src/sheet.tsx
similarity index 99%
rename from components/ui/sheet.tsx
rename to packages/ui/src/sheet.tsx
index cd74d915..20cf478f 100644
--- a/components/ui/sheet.tsx
+++ b/packages/ui/src/sheet.tsx
@@ -5,7 +5,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const Sheet = SheetPrimitive.Root;
diff --git a/components/ui/sonner.tsx b/packages/ui/src/sonner.tsx
similarity index 64%
rename from components/ui/sonner.tsx
rename to packages/ui/src/sonner.tsx
index 02c954b1..41f29e71 100644
--- a/components/ui/sonner.tsx
+++ b/packages/ui/src/sonner.tsx
@@ -9,13 +9,34 @@ import {
} from "lucide-react";
import { Toaster as Sonner, type ToasterProps } from "sonner";
import { useTheme } from "next-themes";
-import { useLocalStorage } from "@/hooks/useLocalStorage";
+import { useEffect, useState } from "react";
+
+const TOAST_POSITION_KEY = "toast-position";
+
+type ToastPosition = "bottom-left" | "bottom-center" | "bottom-right";
+
+const getInitialPosition = (): ToastPosition => {
+ if (typeof window === "undefined") return "bottom-right";
+ const saved = window.localStorage.getItem(TOAST_POSITION_KEY);
+ if (
+ saved === "bottom-left" ||
+ saved === "bottom-center" ||
+ saved === "bottom-right"
+ ) {
+ return saved;
+ }
+ return "bottom-right";
+};
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
- const [toastPosition] = useLocalStorage<
- "bottom-left" | "bottom-center" | "bottom-right"
- >("toast-position", "bottom-right");
+ const [toastPosition, setToastPosition] = useState(
+ "bottom-right",
+ );
+
+ useEffect(() => {
+ setToastPosition(getInitialPosition());
+ }, []);
return (
) {
return (
diff --git a/components/ui/switch.tsx b/packages/ui/src/switch.tsx
similarity index 97%
rename from components/ui/switch.tsx
rename to packages/ui/src/switch.tsx
index 3bf9946f..f06cb161 100644
--- a/components/ui/switch.tsx
+++ b/packages/ui/src/switch.tsx
@@ -3,7 +3,7 @@
import * as SwitchPrimitive from "@radix-ui/react-switch";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Switch({
className,
diff --git a/components/ui/tabs.tsx b/packages/ui/src/tabs.tsx
similarity index 98%
rename from components/ui/tabs.tsx
rename to packages/ui/src/tabs.tsx
index 10c310e8..2b200945 100644
--- a/components/ui/tabs.tsx
+++ b/packages/ui/src/tabs.tsx
@@ -3,7 +3,7 @@
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Tabs({
className,
diff --git a/components/ui/textarea.tsx b/packages/ui/src/textarea.tsx
similarity index 96%
rename from components/ui/textarea.tsx
rename to packages/ui/src/textarea.tsx
index c31e691b..f0fb562e 100644
--- a/components/ui/textarea.tsx
+++ b/packages/ui/src/textarea.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
diff --git a/components/ui/toast.tsx b/packages/ui/src/toast.tsx
similarity index 99%
rename from components/ui/toast.tsx
rename to packages/ui/src/toast.tsx
index edd4572e..765ce2bf 100644
--- a/components/ui/toast.tsx
+++ b/packages/ui/src/toast.tsx
@@ -5,7 +5,7 @@ import * as ToastPrimitives from "@radix-ui/react-toast";
import { X } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "./utils";
const ToastProvider = ToastPrimitives.Provider;
diff --git a/components/ui/toaster.tsx b/packages/ui/src/toaster.tsx
similarity index 89%
rename from components/ui/toaster.tsx
rename to packages/ui/src/toaster.tsx
index 7d82ed55..d38613a5 100644
--- a/components/ui/toaster.tsx
+++ b/packages/ui/src/toaster.tsx
@@ -7,8 +7,8 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
-} from "@/components/ui/toast";
-import { useToast } from "@/components/ui/use-toast";
+} from "./toast";
+import { useToast } from "./use-toast";
export function Toaster() {
const { toasts } = useToast();
diff --git a/components/ui/use-toast.tsx b/packages/ui/src/use-toast.tsx
similarity index 98%
rename from components/ui/use-toast.tsx
rename to packages/ui/src/use-toast.tsx
index b0a3cfb2..582118c4 100644
--- a/components/ui/use-toast.tsx
+++ b/packages/ui/src/use-toast.tsx
@@ -2,7 +2,7 @@
import * as React from "react";
-import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
+import type { ToastActionElement, ToastProps } from "./toast";
const TOAST_LIMIT = 5;
const TOAST_REMOVE_DELAY = 5000;
diff --git a/packages/ui/src/utils.ts b/packages/ui/src/utils.ts
new file mode 100644
index 00000000..a5ef1935
--- /dev/null
+++ b/packages/ui/src/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
deleted file mode 100644
index b3692b66..00000000
--- a/tailwind.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import type { Config } from "tailwindcss";
-
-export default {} satisfies Config;
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 00000000..4672105c
--- /dev/null
+++ b/turbo.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "tasks": {
+ "dev": {
+ "cache": false,
+ "persistent": true
+ },
+ "build": {
+ "dependsOn": [
+ "^build"
+ ],
+ "outputs": [
+ ".next/**",
+ "!.next/cache/**"
+ ],
+ "env": [
+ "CLERK_SECRET_KEY",
+ "CLERK_FRONTEND_API",
+ "SALT",
+ "POSTGRES_URL",
+ "CRON_SECRET",
+ "ATPROTO_SESSION_SECRET"
+ ]
+ },
+ "start": {
+ "cache": false,
+ "persistent": true
+ },
+ "generate:locales": {
+ "outputs": [
+ "src/locales.ts"
+ ]
+ },
+ "generate:oauth-metadata": {
+ "outputs": [
+ "public/oauth-client-metadata.json"
+ ]
+ }
+ }
+}