auth support
This commit is contained in:
parent
b7ab3b322a
commit
65bd65a27e
27 changed files with 1740 additions and 585 deletions
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
BETTER_AUTH_SECRET=YOUR_SECRET
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
REDIS_URL=redis://username:password@localhost:6379
|
||||
BETTER_AUTH_BASE_URL=YOUR_HOST
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -36,4 +36,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||
.vercel
|
||||
|
||||
# misc
|
||||
.tanstack
|
||||
.tanstack
|
||||
*.db
|
||||
railpack-info.json
|
||||
railpack-plan.json
|
||||
database/migrations/
|
||||
66
Dockerfile
Normal file
66
Dockerfile
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Build stage
|
||||
FROM oven/bun:1 AS src
|
||||
|
||||
# Set build arguments
|
||||
ARG BETTER_AUTH_SECRET
|
||||
ARG DATABASE_URL
|
||||
ARG REDIS_URL
|
||||
|
||||
# Set environment variables for build
|
||||
ENV BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET
|
||||
ENV DATABASE_URL=$DATABASE_URL
|
||||
ENV REDIS_URL=$REDIS_URL
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Install lightningcss for Linux
|
||||
# RUN bun add -D lightningcss-linux-x64-gnu
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN bun run nitro:build
|
||||
|
||||
# Production stage
|
||||
FROM oven/bun:1-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built application
|
||||
|
||||
# 01 - Drizzle DB
|
||||
COPY --from=src /app/database ./database
|
||||
|
||||
# 02 - Dependencies
|
||||
COPY --from=src /app/node_modules ./node_modules
|
||||
|
||||
# 03 - Static content
|
||||
COPY --from=src /app/public ./public
|
||||
|
||||
# 04 - Application
|
||||
COPY --from=src /app/src ./src
|
||||
|
||||
# 05 - Utilities
|
||||
COPY --from=src /app/utils ./utils
|
||||
|
||||
# 06 - Package
|
||||
COPY --from=src /app/package.json ./package.json
|
||||
|
||||
# 07 - Output
|
||||
COPY --from=src /app/.output ./.output
|
||||
|
||||
# Set runtime environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "run", "nitro:serve"]
|
||||
50
better-auth.config.ts
Normal file
50
better-auth.config.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { dbSqlite, dbPostgres } from "./database/";
|
||||
import * as authSchema from "./database/schema/better-auth";
|
||||
import { username, admin, apiKey, openAPI } from "better-auth/plugins";
|
||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||
import * as secStorage from "./better-auth.session";
|
||||
import dc from "./drizzle.config";
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [username(), admin({ adminRoles: ["admin"] }), apiKey(), openAPI(), tanstackStartCookies()],
|
||||
basePath: "/auth",
|
||||
database:
|
||||
dc.dialect === "postgresql"
|
||||
? drizzleAdapter(dbPostgres(), {
|
||||
provider: "pg",
|
||||
schema: authSchema,
|
||||
})
|
||||
: drizzleAdapter(dbSqlite(), {
|
||||
provider: "sqlite",
|
||||
schema: authSchema,
|
||||
}),
|
||||
secondaryStorage: process.env.REDIS_URL ? secStorage.storageRedis : secStorage.storageMem,
|
||||
});
|
||||
|
||||
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: elysiajs told me
|
||||
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema());
|
||||
|
||||
export const bAuthOpenAPI = {
|
||||
getPaths: (prefix = "/auth/api") =>
|
||||
getSchema().then(({ paths }) => {
|
||||
const reference: typeof paths = Object.create(null);
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
const key = prefix + path;
|
||||
reference[key] = paths[path];
|
||||
|
||||
for (const method of Object.keys(paths[path])) {
|
||||
const operation = (reference[key] as any)[method];
|
||||
|
||||
operation.tags = ["Better Auth"];
|
||||
}
|
||||
}
|
||||
|
||||
return reference;
|
||||
}) as Promise<any>,
|
||||
components: getSchema().then(({ components }) => components) as Promise<any>,
|
||||
} as const;
|
||||
70
better-auth.session.ts
Normal file
70
better-auth.session.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Store sessions to memory (lost on restart)
|
||||
import { MemoryLevel } from "memory-level";
|
||||
import { RedisClient } from "bun"
|
||||
|
||||
interface SecondaryStorage {
|
||||
get: (key: string) => Promise<unknown>;
|
||||
set: (key: string, value: string, ttl?: number) => Promise<void>;
|
||||
delete: (key: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/* 01 - Memory storage */
|
||||
const memAuthSession = new MemoryLevel();
|
||||
export const storageMem: SecondaryStorage = {
|
||||
get: async (key) => {
|
||||
if (await memAuthSession.has(`${key}_exp`)) {
|
||||
const cur = Math.floor(Date.now() / 1000);
|
||||
const exp = parseInt(await memAuthSession.get(`${key}_exp`) ?? "0");
|
||||
if (cur >= exp) return null;
|
||||
}
|
||||
const value = await memAuthSession.get(key);
|
||||
return value ? value : null;
|
||||
},
|
||||
set: async (key, value, ttl) => {
|
||||
if (ttl) await memAuthSession.put(`${key}_exp`, Math.floor((Date.now() / 1000) + ttl).toString());
|
||||
await memAuthSession.put(key, value);
|
||||
},
|
||||
delete: async (key) => {
|
||||
await memAuthSession.del(key);
|
||||
if (await memAuthSession.has(`${key}_exp`)) await memAuthSession.del(`${key}_exp`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
const cur = Math.floor(Date.now() / 1000);
|
||||
|
||||
for await (const k of memAuthSession.keys()) {
|
||||
if (k.endsWith("_exp")) {
|
||||
const exp = parseInt(await memAuthSession.get(k) ?? "0");
|
||||
|
||||
if (cur >= exp) {
|
||||
await memAuthSession.del(k);
|
||||
await memAuthSession.del(k.slice(0, -4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(cleanup, 10000);
|
||||
|
||||
/* 02 - Redis storage (VK) */
|
||||
const redisInstance: RedisClient | null = process.env.REDIS_URL ? new RedisClient(process.env.REDIS_URL) : null;
|
||||
|
||||
export const storageRedis: SecondaryStorage = {
|
||||
get: async (key) => {
|
||||
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
|
||||
return await redisInstance.get(key);
|
||||
},
|
||||
set: async (key, value, ttl) => {
|
||||
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
|
||||
if (ttl) {
|
||||
await redisInstance.setex(key, ttl, value);
|
||||
} else {
|
||||
await redisInstance.set(key, value);
|
||||
}
|
||||
},
|
||||
delete: async (key) => {
|
||||
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
|
||||
await redisInstance.del(key);
|
||||
}
|
||||
}
|
||||
18
database/index.ts
Normal file
18
database/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Database from "bun:sqlite";
|
||||
import { drizzle as drizzleSqlite } from "drizzle-orm/bun-sqlite";
|
||||
import { drizzle as drizzlePsql } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const SCHEMA = {};
|
||||
|
||||
export function dbSqlite() {
|
||||
const sqlite = new Database(process.env.DATABASE_URL);
|
||||
return drizzleSqlite(sqlite, {schema: SCHEMA});
|
||||
}
|
||||
|
||||
export function dbPostgres() {
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
return drizzlePsql({ client: pool, schema: SCHEMA });
|
||||
}
|
||||
118
database/schema/better-auth.ts
Normal file
118
database/schema/better-auth.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
username: text("username").unique(),
|
||||
displayUsername: text("display_username"),
|
||||
role: text("role"),
|
||||
banned: boolean("banned").default(false),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
});
|
||||
|
||||
export const account = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
);
|
||||
|
||||
export const apikey = pgTable(
|
||||
"apikey",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name"),
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
enabled: boolean("enabled").default(true),
|
||||
rateLimitEnabled: boolean("rate_limit_enabled").default(true),
|
||||
rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000),
|
||||
rateLimitMax: integer("rate_limit_max").default(10),
|
||||
requestCount: integer("request_count").default(0),
|
||||
remaining: integer("remaining"),
|
||||
lastRequest: timestamp("last_request"),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
permissions: text("permissions"),
|
||||
metadata: text("metadata"),
|
||||
},
|
||||
(table) => [
|
||||
index("apikey_key_idx").on(table.key),
|
||||
index("apikey_userId_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
accounts: many(account),
|
||||
apikeys: many(apikey),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [apikey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
16
drizzle.config.ts
Normal file
16
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import "dotenv/config";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("Missing DATABASE_URL in .env file");
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: "./database/schema/*",
|
||||
out: "./database/migrations",
|
||||
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
}
|
||||
});
|
||||
2
mise.toml
Normal file
2
mise.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[tools]
|
||||
bun = "latest"
|
||||
11
nixpacks.toml
Normal file
11
nixpacks.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[phases.setup]
|
||||
nixPkgs = ["bun", "nodejs"]
|
||||
|
||||
[phases.install]
|
||||
cmds = ["bun install --frozen-lockfile"]
|
||||
|
||||
[phases.build]
|
||||
cmds = ["bun run nitro:build"]
|
||||
|
||||
[start]
|
||||
cmd = "bun run nitro:serve"
|
||||
57
package.json
57
package.json
|
|
@ -4,20 +4,25 @@
|
|||
"scripts": {
|
||||
"dev": "bun --bun vite dev",
|
||||
"build": "bun --bun vite build",
|
||||
"serve": "bun --bun vite preview",
|
||||
"serve": "bun run db:push && bun --bun vite preview",
|
||||
"visualize": "bun --bun vite-bundle-visualizer",
|
||||
"nitro-build": "NITRO=1 bun --bun vite build",
|
||||
"nitro-serve": "bun run .output/server/index.mjs",
|
||||
"nitro-visualize": "NITRO=1 bun --bun vite-bundle-visualizer"
|
||||
"nitro:build": "NITRO=1 bun --bun vite build",
|
||||
"nitro:serve": "bun run db:push && bun run .output/server/index.mjs",
|
||||
"nitro:visualize": "NITRO=1 bun --bun vite-bundle-visualizer",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "bun run utils/migrator.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"auth:generate": "bun --bun better-auth generate --config better-auth.config.ts --output database/schema/better-auth.ts -y"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.4.18",
|
||||
"@biomejs/biome": "2.3.6",
|
||||
"@types/bun": "^1.3.2",
|
||||
"@types/react": "^19.2.3",
|
||||
"@types/bun": "^1.3.7",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.3.1",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
|
|
@ -25,27 +30,33 @@
|
|||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.4.5",
|
||||
"@elysiajs/openapi": "^1.4.11",
|
||||
"@elysiajs/eden": "^1.4.6",
|
||||
"@elysiajs/openapi": "^1.4.14",
|
||||
"@elysiajs/static": "^1.4.7",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-form": "^1.25.0",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@tanstack/react-router": "^1.135.2",
|
||||
"@tanstack/react-router-devtools": "^1.136.11",
|
||||
"@tanstack/react-start": "^1.135.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-form": "^1.28.0",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@tanstack/react-router": "^1.157.16",
|
||||
"@tanstack/react-router-devtools": "^1.157.16",
|
||||
"@tanstack/react-start": "^1.157.16",
|
||||
"better-auth": "^1.4.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"elysia": "^1.4.16",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-seed": "^0.3.1",
|
||||
"elysia": "^1.4.22",
|
||||
"lucide-react": "^0.554.0",
|
||||
"nitro": "^3.0.1-alpha.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"memory-level": "^3.1.0",
|
||||
"nitro": "^3.0.1-alpha.2",
|
||||
"pg": "^8.17.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "^5.0.8"
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
railpack.json
Normal file
29
railpack.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://schema.railpack.com",
|
||||
"steps": {
|
||||
"install": {
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "bun install --frozen-lockfile",
|
||||
"customName": "Install dependencies"
|
||||
}
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "bun run nitro:build",
|
||||
"customName": "Build server"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"startCommand": "bun run nitro:serve"
|
||||
}
|
||||
}
|
||||
88
src/lib/Theme.tsx
Normal file
88
src/lib/Theme.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { getCookie, setCookie } from "@tanstack/react-start/server";
|
||||
import { createContext, ReactNode, use, useEffect, useState } from "react";
|
||||
|
||||
export type UserTheme = "light" | "dark" | "system";
|
||||
export type AppTheme = Exclude<UserTheme, "system">;
|
||||
|
||||
const themeCookie = "ui-theme";
|
||||
const themes = ["light", "dark", "system"] as const satisfies UserTheme[];
|
||||
|
||||
export const getStoredTheme = createServerFn().handler(async () => {
|
||||
return (getCookie(themeCookie) || "system") as UserTheme;
|
||||
});
|
||||
|
||||
export const setStoredTheme = createServerFn({ method: "POST" })
|
||||
.inputValidator((data: string) => {
|
||||
if (!themes.includes(data as UserTheme)) {
|
||||
throw new Error("Invalid theme");
|
||||
}
|
||||
return data as UserTheme;
|
||||
})
|
||||
.handler(({ data }) => {
|
||||
setCookie(themeCookie, data);
|
||||
});
|
||||
|
||||
function getSystemTheme(): AppTheme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function handleThemeChange(theme: UserTheme) {
|
||||
const root = document.documentElement;
|
||||
const newTheme = theme === "system" ? getSystemTheme() : theme;
|
||||
root.setAttribute("data-theme", newTheme);
|
||||
}
|
||||
|
||||
function setupPreferredListener() {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = () => handleThemeChange("system");
|
||||
mediaQuery.addEventListener("change", handler);
|
||||
return () => mediaQuery.removeEventListener("change", handler);
|
||||
}
|
||||
|
||||
type ThemeContextProps = {
|
||||
userTheme: UserTheme;
|
||||
appTheme: AppTheme;
|
||||
setTheme: (theme: UserTheme) => void;
|
||||
};
|
||||
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: ReactNode;
|
||||
initialTheme: UserTheme;
|
||||
};
|
||||
export function ThemeProvider({ children, initialTheme }: ThemeProviderProps) {
|
||||
const [userTheme, setUserTheme] = useState<UserTheme>(initialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
handleThemeChange(userTheme);
|
||||
|
||||
if (userTheme === "system") {
|
||||
return setupPreferredListener();
|
||||
}
|
||||
}, [userTheme]);
|
||||
|
||||
const appTheme = userTheme === "system" ? getSystemTheme() : userTheme;
|
||||
|
||||
const setTheme = (newUserTheme: UserTheme) => {
|
||||
setUserTheme(newUserTheme);
|
||||
setStoredTheme({ data: newUserTheme });
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext value={{ userTheme, appTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = use(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
26
src/lib/auth.ts
Normal file
26
src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { createMiddleware, createServerFn } from "@tanstack/react-start";
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient, adminClient, apiKeyClient } from "better-auth/client/plugins"
|
||||
import { auth } from "@/../better-auth.config"
|
||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
usernameClient(),
|
||||
adminClient(),
|
||||
apiKeyClient()
|
||||
],
|
||||
basePath: "/auth"
|
||||
})
|
||||
|
||||
const authMiddle = createMiddleware().server(
|
||||
async ({ next, request }) => {
|
||||
const headers = getRequestHeaders();
|
||||
const session = await auth.api.getSession({ headers })
|
||||
|
||||
return await next({context: {session}})
|
||||
}
|
||||
);
|
||||
|
||||
export const getAuth = createServerFn().middleware([authMiddle]).handler(async ({context}) => {
|
||||
return context.session
|
||||
});
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
const useStore = <T, F>(
|
||||
store: (callback: (state: T) => unknown) => unknown,
|
||||
callback: (state: T) => F,
|
||||
) => {
|
||||
const result = store(callback) as F
|
||||
const [data, setData] = useState<F>()
|
||||
|
||||
useEffect(() => {
|
||||
setData(result)
|
||||
}, [result])
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const useStore = <T, F>(
|
||||
store: (callback: (state: T) => unknown) => unknown,
|
||||
callback: (state: T) => F,
|
||||
) => {
|
||||
const result = store(callback) as F
|
||||
const [data, setData] = useState<F>()
|
||||
|
||||
useEffect(() => {
|
||||
setData(result)
|
||||
}, [result])
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default useStore
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Route as StatsRouteImport } from './routes/stats'
|
|||
import { Route as Otherside_serverRouteImport } from './routes/otherside_server'
|
||||
import { Route as Otherside_clientRouteImport } from './routes/otherside_client'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthSplatRouteImport } from './routes/auth/$'
|
||||
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
||||
|
||||
const StatsRoute = StatsRouteImport.update({
|
||||
|
|
@ -35,6 +36,11 @@ const IndexRoute = IndexRouteImport.update({
|
|||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthSplatRoute = AuthSplatRouteImport.update({
|
||||
id: '/auth/$',
|
||||
path: '/auth/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiSplatRoute = ApiSplatRouteImport.update({
|
||||
id: '/api/$',
|
||||
path: '/api/$',
|
||||
|
|
@ -47,6 +53,7 @@ export interface FileRoutesByFullPath {
|
|||
'/otherside_server': typeof Otherside_serverRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
'/auth/$': typeof AuthSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
|
|
@ -54,6 +61,7 @@ export interface FileRoutesByTo {
|
|||
'/otherside_server': typeof Otherside_serverRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
'/auth/$': typeof AuthSplatRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
|
|
@ -62,6 +70,7 @@ export interface FileRoutesById {
|
|||
'/otherside_server': typeof Otherside_serverRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
'/auth/$': typeof AuthSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
|
|
@ -71,8 +80,15 @@ export interface FileRouteTypes {
|
|||
| '/otherside_server'
|
||||
| '/stats'
|
||||
| '/api/$'
|
||||
| '/auth/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/otherside_client' | '/otherside_server' | '/stats' | '/api/$'
|
||||
to:
|
||||
| '/'
|
||||
| '/otherside_client'
|
||||
| '/otherside_server'
|
||||
| '/stats'
|
||||
| '/api/$'
|
||||
| '/auth/$'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
|
|
@ -80,6 +96,7 @@ export interface FileRouteTypes {
|
|||
| '/otherside_server'
|
||||
| '/stats'
|
||||
| '/api/$'
|
||||
| '/auth/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
|
|
@ -88,6 +105,7 @@ export interface RootRouteChildren {
|
|||
Otherside_serverRoute: typeof Otherside_serverRoute
|
||||
StatsRoute: typeof StatsRoute
|
||||
ApiSplatRoute: typeof ApiSplatRoute
|
||||
AuthSplatRoute: typeof AuthSplatRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -120,6 +138,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/auth/$': {
|
||||
id: '/auth/$'
|
||||
path: '/auth/$'
|
||||
fullPath: '/auth/$'
|
||||
preLoaderRoute: typeof AuthSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/$': {
|
||||
id: '/api/$'
|
||||
path: '/api/$'
|
||||
|
|
@ -136,6 +161,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
Otherside_serverRoute: Otherside_serverRoute,
|
||||
StatsRoute: StatsRoute,
|
||||
ApiSplatRoute: ApiSplatRoute,
|
||||
AuthSplatRoute: AuthSplatRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
// src/routes/__root.tsx
|
||||
/// <reference types="vite/client" />
|
||||
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import {
|
||||
createRootRoute,
|
||||
HeadContent,
|
||||
Link,
|
||||
Outlet,
|
||||
ScriptOnce,
|
||||
Scripts,
|
||||
} from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
|
||||
import appCss from "../../styles.css?url";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { getStoredTheme, ThemeProvider } from "@/lib/Theme";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
|
|
@ -29,7 +36,7 @@ export const Route = createRootRoute({
|
|||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "Bun + TanStack Start Starter",
|
||||
title: "OneStart",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
|
|
@ -37,16 +44,17 @@ export const Route = createRootRoute({
|
|||
{ rel: "icon", href: "/favicon.ico" },
|
||||
],
|
||||
}),
|
||||
loader: async () => ({ _storedTheme: await getStoredTheme() }),
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
notFoundComponent: NotFoundComponent,
|
||||
// ssr: "data-only",
|
||||
pendingMinMs: 2000
|
||||
pendingMinMs: 2000,
|
||||
});
|
||||
|
||||
function NotFoundComponent() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted p-4 antialiased">
|
||||
<Card className="w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Page Not Found</CardTitle>
|
||||
|
|
@ -56,49 +64,12 @@ function NotFoundComponent() {
|
|||
</CardContent>
|
||||
<CardFooter className="flex gap-2 justify-end">
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
Go home
|
||||
</Link>
|
||||
<Link to="/">Go home</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="relative bg-card/80 backdrop-blur-xl text-card-foreground rounded-2xl border border-border/50 shadow-2xl overflow-hidden h-[400px] max-h-4/5 grid grid-rows-[auto_1fr_auto]">
|
||||
<div className="px-8 py-6">
|
||||
<div className="space-y-2 text-center py-2">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">404</h1>
|
||||
<p className="text-lg text-muted-foreground font-medium -mt-2">Page Not Found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 overflow-y-auto">
|
||||
<div className="flex flex-col items-center justify-center py-6 min-h-full">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-10">
|
||||
<div className="pt-6 border-t border-border/30">
|
||||
<Link
|
||||
to="/"
|
||||
className="block w-full px-4 py-2 bg-foreground text-background rounded-lg font-medium hover:opacity-90 transition-opacity text-center text-sm"
|
||||
>
|
||||
← Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
@ -115,13 +86,30 @@ function RootComponent() {
|
|||
}
|
||||
|
||||
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||
const { _storedTheme } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<html>
|
||||
// biome-ignore lint/a11y/useHtmlLang: Template
|
||||
<html suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
<ScriptOnce
|
||||
// biome-ignore lint/correctness/noChildrenProp: Tanstack router related
|
||||
children={`
|
||||
(function() {
|
||||
const storedTheme = ${JSON.stringify(_storedTheme)};
|
||||
if (storedTheme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute("data-theme", systemTheme);
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", storedTheme);
|
||||
}
|
||||
})();
|
||||
`}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ThemeProvider initialTheme={_storedTheme}>{children}</ThemeProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
import { Elysia } from 'elysia'
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||
|
||||
const app = new Elysia({
|
||||
prefix: '/api'
|
||||
}).get('/', 'Hello Elysia!')
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||
|
||||
export const Route = createFileRoute('/api/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const api = createIsomorphicFn()
|
||||
.server(() => treaty(app).api)
|
||||
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
import { app, type treatyClient } from "./-server"
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||
|
||||
export const Route = createFileRoute('/api/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const api = createIsomorphicFn()
|
||||
.server(() => treaty(app).api)
|
||||
.client(() => treaty<treatyClient>('localhost:3000').api)
|
||||
11
src/routes/api/-server/index.ts
Normal file
11
src/routes/api/-server/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Elysia instance
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
import {dbSqlite, dbPostgres} from "@db/index"
|
||||
import dc from "@/../drizzle.config"
|
||||
|
||||
export const app = new Elysia({
|
||||
prefix: '/api'
|
||||
}).decorate("db", dc.dialect === "postgresql" ? dbPostgres() : dbSqlite()).get('/', 'Hello Elysia!')
|
||||
|
||||
export type treatyClient = typeof app
|
||||
13
src/routes/auth/$.ts
Normal file
13
src/routes/auth/$.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { auth } from "@/../better-auth.config";
|
||||
|
||||
const handle = ({ request }: { request: Request }) => auth.handler(request)
|
||||
|
||||
export const Route = createFileRoute('/auth/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle
|
||||
}
|
||||
}
|
||||
})
|
||||
34
src/server.ts
Normal file
34
src/server.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// src/server.ts
|
||||
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
|
||||
import {auth} from "@/../better-auth.config"
|
||||
|
||||
export default createServerEntry({
|
||||
async fetch(request) {
|
||||
/* Needs to be done before handling tanstack */
|
||||
const ctx = await auth.$context;
|
||||
if ((await ctx.internalAdapter.countTotalUsers()) <= 0) {
|
||||
const hash = await ctx.password.hash("admin");
|
||||
|
||||
const myUser = await ctx.internalAdapter.createUser({
|
||||
email: "admin@admin.com",
|
||||
name: "Admin",
|
||||
username: "admin",
|
||||
role: "admin",
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
await ctx.internalAdapter.linkAccount({
|
||||
userId: myUser.id,
|
||||
providerId: "credential",
|
||||
accountId: myUser.id,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Admin account created. Login with username admin and password admin to begin configuring the server.",
|
||||
);
|
||||
}
|
||||
|
||||
return handler.fetch(request)
|
||||
},
|
||||
})
|
||||
571
styles.css
571
styles.css
|
|
@ -1,285 +1,288 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
/* go to tweakcn.com to customize the theme */
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.5645 0.1681 254.3428);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.1450 0 0);
|
||||
--foreground: oklch(0.9850 0 0);
|
||||
--card: oklch(0.2050 0 0);
|
||||
--card-foreground: oklch(0.9850 0 0);
|
||||
--popover: oklch(0.2690 0 0);
|
||||
--popover-foreground: oklch(0.9850 0 0);
|
||||
--primary: oklch(0.5645 0.1681 254.3428);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.2690 0 0);
|
||||
--secondary-foreground: oklch(0.9850 0 0);
|
||||
--muted: oklch(0.2690 0 0);
|
||||
--muted-foreground: oklch(0.7080 0 0);
|
||||
--accent: oklch(0.3710 0 0);
|
||||
--accent-foreground: oklch(0.9850 0 0);
|
||||
--destructive: oklch(0.7040 0.1910 22.2160);
|
||||
--destructive-foreground: oklch(0.9850 0 0);
|
||||
--border: oklch(0.2750 0 0);
|
||||
--input: oklch(0.3250 0 0);
|
||||
--ring: oklch(0.5560 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.2050 0 0);
|
||||
--sidebar-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.2690 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-border: oklch(0.2750 0 0);
|
||||
--sidebar-ring: oklch(0.4390 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
/* default */
|
||||
/*
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
@custom-variant light (&:where([data-theme=light], [data-theme=light] *));
|
||||
|
||||
/* go to tweakcn.com to customize the theme */
|
||||
:where([data-theme=dark], [data-theme=dark]),
|
||||
.dark {
|
||||
--background: oklch(0.1450 0 0);
|
||||
--foreground: oklch(0.9850 0 0);
|
||||
--card: oklch(0.2050 0 0);
|
||||
--card-foreground: oklch(0.9850 0 0);
|
||||
--popover: oklch(0.2690 0 0);
|
||||
--popover-foreground: oklch(0.9850 0 0);
|
||||
--primary: oklch(0.5645 0.1681 254.3428);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.2690 0 0);
|
||||
--secondary-foreground: oklch(0.9850 0 0);
|
||||
--muted: oklch(0.2690 0 0);
|
||||
--muted-foreground: oklch(0.7080 0 0);
|
||||
--accent: oklch(0.3710 0 0);
|
||||
--accent-foreground: oklch(0.9850 0 0);
|
||||
--destructive: oklch(0.7040 0.1910 22.2160);
|
||||
--destructive-foreground: oklch(0.9850 0 0);
|
||||
--border: oklch(0.2750 0 0);
|
||||
--input: oklch(0.3250 0 0);
|
||||
--ring: oklch(0.5560 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.2050 0 0);
|
||||
--sidebar-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.2690 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-border: oklch(0.2750 0 0);
|
||||
--sidebar-ring: oklch(0.4390 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
}
|
||||
|
||||
:where([data-theme=light], [data-theme=light]),
|
||||
.light {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.5645 0.1681 254.3428);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
/* default */
|
||||
/*
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@db/*": ["./database/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
utils/migrator.ts
Normal file
28
utils/migrator.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import "dotenv/config";
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
||||
|
||||
import { Pool } from "pg";
|
||||
import { drizzle as drizzlePSQL } from 'drizzle-orm/node-postgres';
|
||||
import { migrate as migratePSQL } from 'drizzle-orm/node-postgres/migrator';
|
||||
|
||||
import dc from "../drizzle.config";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("Missing DATABASE_URL in .env file");
|
||||
}
|
||||
|
||||
if (dc.dialect === "sqlite") {
|
||||
const sqlite = new Database(process.env.DATABASE_URL);
|
||||
const db = drizzle(sqlite);
|
||||
await migrate(db, { migrationsFolder: 'database/migrations' });
|
||||
console.log("migrate complete");
|
||||
} else if (dc.dialect === "postgresql") {
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
const db = drizzlePSQL({ client: pool });
|
||||
await migratePSQL(db, { migrationsFolder: 'database/migrations' });
|
||||
console.log("migrate complete");
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export default defineConfig({
|
|||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@db": path.resolve(__dirname, "./database")
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue