Compare commits

..

25 Commits

Author SHA1 Message Date
e2e6e4a70a Update path in script 2025-04-03 10:24:23 -05:00
bc9417d36b Clean up Authentik login and login page in general. 2025-01-16 10:55:47 -06:00
be6f80a997 Add back necessary environment variable 2025-01-16 08:58:59 -06:00
902eee260b May have fixed authentik idk 2025-01-16 06:37:22 -06:00
5295d06686 test 2025-01-16 06:18:46 -06:00
ed4d51400c make everything pretty & try to fix authentik 2025-01-15 20:07:04 -06:00
7d637b7401 Fix hopefully? 2025-01-15 16:59:01 -06:00
76c1c40e74 Add authentik 2025-01-15 16:41:21 -06:00
fe3dabe3b7 Set up for docker 2025-01-13 16:36:01 -06:00
0d5197dd94 add dev command 2024-10-05 18:02:42 -05:00
787fcb0031 Fix long statuses. Not perfect but good for now 2024-09-24 11:14:32 -05:00
aab4efbb00 Fix long statuses. Not perfect but good for now 2024-09-24 11:04:34 -05:00
744328c156 Update package.json 2024-07-31 16:00:51 -05:00
f3bb15aaeb Fix date in history for dev environment 2024-07-30 21:02:10 -05:00
d8b1aa242a Make checkbox smaller on mobile 2024-07-30 10:36:51 -05:00
eab772ad99 Fix word wrap with no white spaces for status 2024-07-30 10:25:07 -05:00
0fbd0f7182 Fix word wrap with no white spaces for status 2024-07-30 10:24:08 -05:00
6b80930a86 Make the pagination buttons better by conditionally rendering based on amount of pages 2024-07-30 10:17:43 -05:00
44497ebe7b History Table is now functioning completely 2024-07-30 10:02:27 -05:00
b14383f8fd General History Drawer now works 2024-07-29 14:55:21 -05:00
c95ee5957c General History Drawer now works 2024-07-29 14:54:29 -05:00
8a00507431 Fixes after changing api names 2024-07-25 22:16:16 -05:00
a849b065a1 Update API names now that I can update iOS App 2024-07-25 21:49:38 -05:00
f6af2ff738 Merge branch 'master' of https://git.gibbyb.com/gib/Tech_Tracker_Web 2024-07-25 20:10:20 -05:00
e9a24f0d75 Add env.example 2024-07-25 20:09:35 -05:00
90 changed files with 3777 additions and 3331 deletions

0
.eslintrc.cjs Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all"
}

0
.prod/Dockerfile Normal file → Executable file
View File

View File

@ -1,4 +1,4 @@
cd ~/Documents/Web/Tech_Tracker_Web cd ~/Documents/Web/Tech_Tracker_Web || exit
git pull git pull
pnpm update pnpm update
sudo docker restart techtracker sudo docker restart techtracker

0
README.md Normal file → Executable file
View File

0
components.json Normal file → Executable file
View File

View File

@ -0,0 +1,53 @@
# syntax=docker.io/docker/dockerfile:1
FROM node:18-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env .env.production
RUN npm run build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD HOSTNAME="0.0.0.0" node server.js

View File

@ -0,0 +1,17 @@
services:
techtracker:
build:
context: ../../
dockerfile: docker/development/Dockerfile
image: with-docker-multi-env-development
container_name: techtracker
networks:
- node_apps
ports:
- "3004:3000"
tty: true
restart: unless-stopped
networks:
node_apps:
external: true

View File

@ -0,0 +1,54 @@
# syntax=docker.io/docker/dockerfile:1
FROM node:18-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env .env.production
RUN npm run build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD HOSTNAME="0.0.0.0" node server.js

View File

@ -0,0 +1,16 @@
services:
techtracker:
build:
context: ../../
dockerfile: docker/production/Dockerfile
image: with-docker-multi-env-development
container_name: techtracker
networks:
- node_apps
ports:
- "3004:3000"
tty: true
restart: unless-stopped
networks:
node_apps:
external: true

10
drizzle.config.ts Normal file → Executable file
View File

@ -1,12 +1,12 @@
import { type Config } from "drizzle-kit"; import { type Config } from 'drizzle-kit';
import { env } from "~/env"; import { env } from '~/env';
export default { export default {
schema: "./src/server/db/schema.ts", schema: './src/server/db/schema.ts',
dialect: "mysql", dialect: 'mysql',
dbCredentials: { dbCredentials: {
url: env.DATABASE_URL, url: env.DATABASE_URL,
}, },
tablesFilter: ["tech_tracker_web_*"], tablesFilter: ['tech_tracker_web_*'],
} satisfies Config; } satisfies Config;

16
env.example Normal file
View File

@ -0,0 +1,16 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL=""
# API Key
API_KEY=""
# Auth.js
#NEXTAUTH_SECRET=""
AUTH_SECRET=""
# Entra - https://authjs.dev/getting-started/providers/microsoft-entra-id
AUTH_MICROSOFT_ENTRA_ID_ID=""
AUTH_MICROSOFT_ENTRA_ID_SECRET=""
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID=""

View File

@ -2,36 +2,48 @@
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds. * for Docker builds.
*/ */
await import("./src/env.js"); import './src/env.js';
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
async headers() { output: 'standalone',
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: cspHeader.replace(/\n/g, ''),
},
],
},
];
},
}; };
export default config; export default config;
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
//await import("./src/env.js");
//const cspHeader = `
//default-src 'self';
//script-src 'self' 'unsafe-eval' 'unsafe-inline';
//style-src 'self' 'unsafe-inline';
//img-src 'self' blob: data:;
//font-src 'self';
//object-src 'none';
//base-uri 'self';
//form-action 'self';
//frame-ancestors 'none';
//upgrade-insecure-requests;
//`
//[>* @type {import("next").NextConfig} <]
//const config = {
//async headers() {
//return [
//{
//source: "/(.*)",
//headers: [
//{
//key: "Content-Security-Policy",
//value: cspHeader.replace(/\n/g, ''),
//},
//],
//},
//];
//},
//};
//export default config;

82
package.json Normal file → Executable file
View File

@ -12,64 +12,66 @@
"dev": "next dev", "dev": "next dev",
"lint": "next lint", "lint": "next lint",
"start": "next start", "start": "next start",
"go": "git pull && next build && next start" "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.10.0",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@t3-oss/env-nextjs": "^0.10.1", "@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.4",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"drizzle-orm": "^0.30.10", "drizzle-orm": "^0.30.10",
"geist": "^1.3.1", "geist": "^1.3.1",
"lucide-react": "^0.411.0", "lucide-react": "^0.411.0",
"mysql2": "^3.10.3", "mysql2": "^3.12.0",
"next": "^14.2.5", "next": "^14.2.23",
"next-auth": "5.0.0-beta.19", "next-auth": "5.0.0-beta.19",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"pm2": "^5.4.2", "pm2": "^5.4.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^9.0.3", "react-day-picker": "^9.5.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.54.2",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sharp": "^0.33.4", "sharp": "^0.33.5",
"sonner": "^1.5.0", "sonner": "^1.7.2",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1", "vaul": "^0.9.9",
"zod": "^3.23.8" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/eslint": "^8.56.11", "@types/eslint": "^8.56.12",
"@types/node": "^20.14.12", "@types/node": "^20.17.14",
"@types/react": "^18.3.3", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^7.17.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.17.0", "@typescript-eslint/parser": "^7.18.0",
"drizzle-kit": "^0.21.4", "drizzle-kit": "^0.21.4",
"eslint": "^8.57.0", "eslint": "^8.57.1",
"eslint-config-next": "^14.2.5", "eslint-config-next": "^14.2.23",
"eslint-plugin-drizzle": "^0.2.3", "eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.39", "postcss": "^8.5.1",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.10",
"tailwindcss": "^3.4.6", "tailwindcss": "^3.4.17",
"typescript": "^5.5.4" "typescript": "^5.7.3"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.36.1" "initVersion": "7.36.1"

3885
pnpm-lock.yaml generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
postcss.config.cjs Normal file → Executable file
View File

2
prettier.config.js Normal file → Executable file
View File

@ -1,6 +1,6 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = { const config = {
plugins: ["prettier-plugin-tailwindcss"], plugins: ['prettier-plugin-tailwindcss'],
}; };
export default config; export default config;

0
public/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 994.71 151.65"><defs><style>.cls-1{fill:#fd4b2d;}</style></defs><path class="cls-1" d="M284.72,50.4H305.5v82.84H284.72v-8.76a40.79,40.79,0,0,1-12.21,8.34,34.14,34.14,0,0,1-13.27,2.55q-16.05,0-27.76-12.45T219.77,92q0-19.18,11.33-31.45t27.53-12.26a34.94,34.94,0,0,1,14,2.82,38.32,38.32,0,0,1,12.1,8.45ZM262.87,67.45a21,21,0,0,0-16,6.82q-6.37,6.81-6.38,17.47T247,109.4a21,21,0,0,0,16,6.93,21.42,21.42,0,0,0,16.24-6.81q6.45-6.81,6.45-17.86,0-10.8-6.45-17.51A21.71,21.71,0,0,0,262.87,67.45Z"/><path class="cls-1" d="M335.8,50.4h21V90.29q0,11.65,1.6,16.18a14.16,14.16,0,0,0,5.16,7,14.76,14.76,0,0,0,8.74,2.51,15.25,15.25,0,0,0,8.81-2.48,14.49,14.49,0,0,0,5.38-7.27q1.31-3.57,1.3-15.3V50.4h20.79V85.5q0,21.69-3.43,29.69a32.32,32.32,0,0,1-12.33,15q-8.16,5.22-20.71,5.22-13.64,0-22.05-6.09a32.2,32.2,0,0,1-11.84-17q-2.43-7.55-2.43-27.41Z"/><path class="cls-1" d="M441.32,19.86H462.1V50.4h12.34V68.29H462.1v65H441.32V68.29H430.66V50.4h10.66Z"/><path class="cls-1" d="M495,18.42h20.63V58.77a47.41,47.41,0,0,1,12.26-7.88,31.62,31.62,0,0,1,12.49-2.63,28.13,28.13,0,0,1,20.78,8.53q7.23,7.4,7.24,21.7v54.75H547.9V96.92q0-14.4-1.37-19.49a13.6,13.6,0,0,0-4.68-7.62,13.19,13.19,0,0,0-8.18-2.51,15.43,15.43,0,0,0-10.85,4.19,22.14,22.14,0,0,0-6.28,11.42q-.91,3.72-.92,17v33.28H495Z"/><path class="cls-1" d="M680.84,97.83H614.06a22.25,22.25,0,0,0,7.73,14q6.29,5.22,16,5.21a27.7,27.7,0,0,0,20-8.14l17.51,8.22a41.31,41.31,0,0,1-15.68,13.74q-9.13,4.46-21.7,4.46-19.5,0-31.75-12.3T594,92.27q0-19,12.22-31.48t30.65-12.53q19.56,0,31.82,12.53t12.26,33.08ZM660.05,81.46a20.87,20.87,0,0,0-8.12-11.27,23.61,23.61,0,0,0-14.08-4.34,24.88,24.88,0,0,0-15.25,4.88q-4.11,3-7.62,10.73Z"/><path class="cls-1" d="M707,50.4H727.8v8.49a50.15,50.15,0,0,1,12.81-8.3,31.08,31.08,0,0,1,11.75-2.33,28.44,28.44,0,0,1,20.91,8.61q7.22,7.31,7.22,21.62v54.75H759.93V97q0-14.83-1.33-19.7A13.48,13.48,0,0,0,754,69.85a13,13,0,0,0-8.16-2.55A15.32,15.32,0,0,0,735,71.52a22.6,22.6,0,0,0-6.27,11.67q-.9,3.89-.91,16.81v33.24H707Z"/><path class="cls-1" d="M812.46,19.86h20.79V50.4h12.33V68.29H833.25v65H812.46V68.29H801.8V50.4h10.66Z"/><path class="cls-1" d="M874.16,16.29a12.74,12.74,0,0,1,9.38,3.95,13.18,13.18,0,0,1,3.91,9.6,13,13,0,0,1-3.87,9.48,12.6,12.6,0,0,1-9.27,3.92,12.73,12.73,0,0,1-9.45-4A13.39,13.39,0,0,1,861,29.53a12.78,12.78,0,0,1,3.87-9.36A12.71,12.71,0,0,1,874.16,16.29Z"/><rect class="cls-1" x="863.77" y="50.4" width="20.79" height="82.84"/><path class="cls-1" d="M913,18.42h20.78V84.55L964.34,50.4h26.11L954.76,90.1l40,43.14h-25.8L933.73,95.06v38.18H913Z"/><rect class="cls-1" x="107.1" y="34.93" width="6.37" height="18.2"/><rect class="cls-1" x="123.67" y="34.16" width="6.37" height="14.23"/><path class="cls-1" d="M30.83,55A23.23,23.23,0,0,0,10.41,67.13h10.8C26,63,32.94,61.8,38,67.13H49.39C44.93,61.09,38.24,55,30.83,55Z"/><path class="cls-1" d="M46.25,78.11c-14.89,31.15-41,4.6-25-11H10.41c-8.47,14.76,3.24,34.68,20.42,34.23,13.28,0,24.24-19.72,24.24-23.21,0-1.54-2.14-6.25-5.68-11H38A40.52,40.52,0,0,1,46.25,78.11Zm.4-.91Z"/><path class="cls-1" d="M189.62,34.71V117A28.62,28.62,0,0,1,161,145.54H148.89v-28H90.94v28H78.81A28.62,28.62,0,0,1,50.22,117V91.08h91.87V41.62H97.74V69.41H50.22V34.71a27.43,27.43,0,0,1,.19-3.29,27.09,27.09,0,0,1,.71-3.84c.1-.41.22-.82.34-1.21a2.13,2.13,0,0,1,.09-.3c.07-.21.13-.4.2-.59s.14-.4.21-.59.16-.44.25-.65.18-.43.26-.64a29.35,29.35,0,0,1,2.6-4.82l0-.05c.26-.37.53-.75.81-1.12s.47-.61.7-.91.57-.67.86-1,.56-.63.86-.93l0,0a4.53,4.53,0,0,1,.49-.49,29.23,29.23,0,0,1,3.4-2.84c.32-.24.66-.46,1-.68s.77-.49,1.17-.72a23.78,23.78,0,0,1,2.29-1.21l.75-.34a27.84,27.84,0,0,1,3.35-1.21c.44-.13.88-.24,1.33-.35a6.19,6.19,0,0,1,.65-.15,28.86,28.86,0,0,1,3.87-.57l.56,0h.28c.43,0,.87,0,1.31,0H161c.43,0,.87,0,1.3,0h.28l.56,0a29.25,29.25,0,0,1,3.88.57c.22,0,.43.09.65.15.45.11.88.22,1.32.35a27.23,27.23,0,0,1,3.35,1.21l.75.34a25.19,25.19,0,0,1,2.3,1.21c.39.23.78.47,1.16.72s.69.44,1,.68a29.23,29.23,0,0,1,3.91,3.36q.45.45.87.93c.29.32.57.66.85,1l.71.91c.28.37.54.75.8,1.12l0,.05a28.61,28.61,0,0,1,2.6,4.82l.27.64.24.65c.08.19.15.39.22.59l.19.59c0,.09.06.19.1.3.11.39.23.8.34,1.21a28.56,28.56,0,0,1,.7,3.84A27.42,27.42,0,0,1,189.62,34.71Z"/><path class="cls-1" d="M184.76,18.78H55.07A28.59,28.59,0,0,1,78.8,6.12H161A28.59,28.59,0,0,1,184.76,18.78Z"/><path class="cls-1" d="M189.43,31.43H50.4a28.29,28.29,0,0,1,4.67-12.65H184.76A28.17,28.17,0,0,1,189.43,31.43Z"/><path class="cls-1" d="M189.63,34.71v9.37H142.09V41.62H97.74v2.46H50.21V34.71a27.43,27.43,0,0,1,.19-3.29h139A27.42,27.42,0,0,1,189.63,34.71Z"/><rect class="cls-1" x="50.21" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="142.09" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="50.21" y="56.74" width="47.54" height="12.65"/><rect class="cls-1" x="142.09" y="56.74" width="47.54" height="12.65"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

0
public/images/default_user_pfp.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

0
public/images/exit_fullscreen.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

0
public/images/fullscreen.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

0
public/images/gitea_logo.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

0
public/images/microsoft_logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" width="779.07" height="141.73" viewBox="0 0 779.07 141.73"><title>Microsoft365_logo_horiz_c-gray_cmyk_horiz_c-gray_cmyk</title><g id="MS-symbol"><g><path d="M608.2,69.69v.19c5,.58,8.89,2.32,11.76,5.23a15,15,0,0,1,4.32,11,18.7,18.7,0,0,1-6.89,15.15q-6.9,5.75-18.74,5.74a41.61,41.61,0,0,1-9.44-1.08,23,23,0,0,1-7.08-2.62V92.11a25,25,0,0,0,7.56,4,27,27,0,0,0,8.71,1.51q6.44,0,10.19-3a9.94,9.94,0,0,0,3.72-8.14,9.39,9.39,0,0,0-4.41-8.39c-2.95-1.93-7.15-2.89-12.63-2.89h-6V65.61h5.71q7.26,0,11.18-2.7A8.92,8.92,0,0,0,610,55a9,9,0,0,0-3-7.32c-2-1.71-4.92-2.57-8.67-2.56a20.68,20.68,0,0,0-7,1.22,24.17,24.17,0,0,0-6.58,3.67V39.59a28.42,28.42,0,0,1,7.45-2.8,39.73,39.73,0,0,1,9.09-1q9,0,14.82,4.66a14.89,14.89,0,0,1,5.78,12.11,16.73,16.73,0,0,1-3.55,11A18.89,18.89,0,0,1,608.2,69.69Z" fill="#737474"></path><path d="M643,70a12.92,12.92,0,0,1,6.1-5.76,20.9,20.9,0,0,1,9.17-2,20,20,0,0,1,14,5.45q5.88,5.46,5.88,15.84,0,10.95-6.74,17.19a23.27,23.27,0,0,1-16.4,6.24q-11.28,0-17.76-8.38T630.8,75.22q0-19.5,8.61-29.48a27.4,27.4,0,0,1,21.64-10,50.12,50.12,0,0,1,7.24.47,18.64,18.64,0,0,1,5.33,1.49V48.13a26.26,26.26,0,0,0-6-2.26,25.55,25.55,0,0,0-6-.74A16.55,16.55,0,0,0,648,51.65q-5.15,6.52-5.24,18.32Zm.19,13.54a14.92,14.92,0,0,0,3.37,9.92,10.7,10.7,0,0,0,8.55,4A10.54,10.54,0,0,0,663.33,94q3.15-3.47,3.14-9.53,0-6.42-3.08-9.72A10.86,10.86,0,0,0,655,71.47a11.47,11.47,0,0,0-8.57,3.33,11.93,11.93,0,0,0-3.23,8.76Z" fill="#737474"></path><path d="M725.94,84.33q0,10.24-6.86,16.49T700.21,107a35.2,35.2,0,0,1-9.46-1.23,32.77,32.77,0,0,1-7-2.66V92.3a25.77,25.77,0,0,0,7.6,4,24.67,24.67,0,0,0,7.52,1.27q6.87,0,11-3.41A11.33,11.33,0,0,0,714,84.91,10.5,10.5,0,0,0,709.79,76q-4.24-3.18-12.22-3.17c-1.57,0-3.62.07-6.17.21s-4.3.28-5.26.41L688.59,37h34.23v9.86H698.19l-1.13,16.77c1.34-.11,2.38-.16,3.09-.17h3Q714,63.41,720,69T725.94,84.33Z" fill="#737474"></path></g><path d="M239.64,37v68.84h-12V51.87h-.19l-21.36,54h-7.92L176.29,51.87h-.14v54h-11V37h17.14L202,88h.29l20.89-51Zm10,5.23a6.33,6.33,0,0,1,2.09-4.82,7.41,7.41,0,0,1,10.06,0,6.58,6.58,0,0,1,2,4.78A6.21,6.21,0,0,1,261.74,47a7.07,7.07,0,0,1-5,1.92,7,7,0,0,1-5-1.93,6.29,6.29,0,0,1-2.08-4.74Zm12.82,14.27v49.37H250.82V56.48ZM297.7,97.37a15.76,15.76,0,0,0,5.67-1.19A24,24,0,0,0,309.13,93v10.81a23.43,23.43,0,0,1-6.31,2.4,34.86,34.86,0,0,1-7.76.82q-10.89,0-17.7-6.9t-6.82-17.59q0-11.89,7-19.56t19.72-7.8a27.28,27.28,0,0,1,6.61.84,22,22,0,0,1,5.3,2V69.17a23.31,23.31,0,0,0-5.5-3A15.8,15.8,0,0,0,297.9,65a14.55,14.55,0,0,0-11.09,4.46q-4.23,4.47-4.22,12.06t4.05,11.66q4,4.17,11,4.15Zm44.51-41.71a14,14,0,0,1,2.5.19,10,10,0,0,1,1.86.48V68.09a10.17,10.17,0,0,0-2.66-1.27,13.33,13.33,0,0,0-4.25-.6,9.05,9.05,0,0,0-7.23,3.6q-3,3.6-2.95,11.09v24.91H317.9V56.45h11.61v7.77h.2A13.55,13.55,0,0,1,334.48,58,13,13,0,0,1,342.21,55.66Zm5,26.21q0-12.24,6.92-19.39t19.21-7.16q11.57,0,18.07,6.89t6.52,18.63q0,12-6.91,19.11t-18.82,7.1q-11.47,0-18.21-6.74T347.2,81.87Zm12.12-.39q0,7.74,3.5,11.81t10,4.08q6.34,0,9.65-4.08t3.32-12.11q0-8-3.44-12t-9.62-4.07q-6.39,0-9.91,4.25t-3.55,12.14Zm55.89-12a5,5,0,0,0,1.59,3.91q1.59,1.41,7,3.58,7,2.79,9.76,6.26a12.91,12.91,0,0,1,2.8,8.38A13.52,13.52,0,0,1,431,102.75Q425.66,107,416.54,107a35.4,35.4,0,0,1-6.8-.75,30.18,30.18,0,0,1-6.3-1.86V93a28.2,28.2,0,0,0,6.82,3.5,19.93,19.93,0,0,0,6.62,1.3,11.83,11.83,0,0,0,5.8-1.1,4,4,0,0,0,1.87-3.73,5.11,5.11,0,0,0-1.94-4.05,29,29,0,0,0-7.37-3.82q-6.45-2.69-9.13-6a13.21,13.21,0,0,1-2.68-8.55,13.47,13.47,0,0,1,5.3-11q5.31-4.3,13.76-4.31a33.14,33.14,0,0,1,5.8.57,25.88,25.88,0,0,1,5.38,1.49V68.33a25.11,25.11,0,0,0-5.38-2.64,17.89,17.89,0,0,0-6.05-1.11,8.82,8.82,0,0,0-5.16,1.31,4.1,4.1,0,0,0-1.9,3.55Zm26.17,12.43q0-12.24,6.91-19.39t19.2-7.16q11.58,0,18.08,6.89t6.52,18.63q0,12-6.91,19.11t-18.83,7.1q-11.48,0-18.21-6.74t-6.79-18.44Zm12.11-.39q0,7.74,3.51,11.81T467,97.37q6.33,0,9.65-4.08t3.31-12.11q0-8-3.43-12t-9.63-4.07q-6.37,0-9.91,4.25t-3.49,12.14ZM530.64,66H513.29v39.84H501.53V66h-8.26v-9.5h8.26V49.6a17.06,17.06,0,0,1,5.06-12.74,17.82,17.82,0,0,1,13-5,29.06,29.06,0,0,1,3.73.22,15,15,0,0,1,2.88.65v10a13.26,13.26,0,0,0-2-.82,10.54,10.54,0,0,0-3.3-.47,7,7,0,0,0-5.59,2.28q-2,2.28-2,6.74v6h17.3V45.38l11.67-3.55V56.48H554V66H542.26V89.07q0,4.56,1.65,6.43t5.21,1.87a7.66,7.66,0,0,0,2.42-.48A11.92,11.92,0,0,0,554,95.73v9.61a14.24,14.24,0,0,1-3.67,1.15,26.12,26.12,0,0,1-5.07.53q-7.35,0-11-3.92t-3.67-11.78Z" fill="#737474"></path><rect x="15.94" y="14.03" width="54.53" height="54.53" fill="#f05125"></rect><rect x="76.14" y="14.03" width="54.53" height="54.53" fill="#7ebb42"></rect><rect x="15.94" y="74.24" width="54.53" height="54.53" fill="#33a0da"></rect><rect x="76.14" y="74.24" width="54.53" height="54.53" fill="#fdb813"></rect></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

0
public/images/tech_tracker_appicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

0
public/images/tech_tracker_favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

0
public/images/tech_tracker_logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 386 KiB

105
scripts/files_to_clipboard.py Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
import os
import sys
import argparse
from pathlib import Path
import pyperclip
import questionary
# List of directories to exclude
EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'}
def collect_files(project_path):
"""
Collects files from the project directory, excluding specified directories and filtering by extensions.
Returns a list of file paths relative to the project directory.
"""
collected_files = []
for root, dirs, files in os.walk(project_path):
# Exclude specified directories
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
for file in files:
file_path = Path(root) / file
relative_path = file_path.relative_to(project_path)
collected_files.append(relative_path)
return collected_files
def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Generate Markdown from selected files.')
parser.add_argument('path', nargs='?', default='.', help='Path to the project directory')
args = parser.parse_args()
project_path = Path(args.path).resolve()
if not project_path.is_dir():
print(f"Error: '{project_path}' is not a directory.")
sys.exit(1)
# Collect files from the project directory
file_list = collect_files(project_path)
if not file_list:
print("No files found in the project directory with the specified extensions.")
sys.exit(1)
# Sort file_list for better organization
file_list.sort()
# Interactive file selection using questionary
print("\nSelect the files you want to include:")
selected_files = questionary.checkbox(
"Press space to select files, and Enter when you're done:",
choices=[str(f) for f in file_list]
).ask()
if not selected_files:
print("No files selected.")
sys.exit(1)
# Generate markdown
markdown_lines = []
markdown_lines.append('')
for selected_file in selected_files:
file_path = project_path / selected_file
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Determine the language for code block from file extension
language = file_path.suffix.lstrip('.')
markdown_lines.append(f'{selected_file}')
markdown_lines.append(f'```{language}')
markdown_lines.append(content)
markdown_lines.append('```')
markdown_lines.append('')
except Exception as e:
print(f"Error reading file {selected_file}: {e}")
markdown_text = '\n'.join(markdown_lines)
# Write markdown to file
output_file = 'output.md'
with open(output_file, 'w', encoding='utf-8') as f:
f.write(markdown_text)
print(f"\nMarkdown file '{output_file}' has been generated.")
# Copy markdown content to clipboard
pyperclip.copy(markdown_text)
print("Markdown content has been copied to the clipboard.")
if __name__ == "__main__":
# Check if required libraries are installed
try:
import questionary
import pyperclip
except ImportError as e:
missing_module = e.name
print(f"Error: Missing required module '{missing_module}'.")
print(f"Please install it by running: pip install {missing_module}")
sys.exit(1)
main()

View File

@ -0,0 +1,18 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
import './src/env.js';
/** @type {import("next").NextConfig} */
const config = {
output: 'standalone',
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
};
export default config;

View File

@ -0,0 +1,49 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
import './src/env.js';
/** @type {import("next").NextConfig} */
const config = {
output: 'standalone',
};
export default config;
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
//await import("./src/env.js");
//const cspHeader = `
//default-src 'self';
//script-src 'self' 'unsafe-eval' 'unsafe-inline';
//style-src 'self' 'unsafe-inline';
//img-src 'self' blob: data:;
//font-src 'self';
//object-src 'none';
//base-uri 'self';
//form-action 'self';
//frame-ancestors 'none';
//upgrade-insecure-requests;
//`
//[>* @type {import("next").NextConfig} <]
//const config = {
//async headers() {
//return [
//{
//source: "/(.*)",
//headers: [
//{
//key: "Content-Security-Policy",
//value: cspHeader.replace(/\n/g, ''),
//},
//],
//},
//];
//},
//};
//export default config;

7
scripts/reload_container.sh Executable file
View File

@ -0,0 +1,7 @@
git pull
mv ./next.config.js ./scripts/next.config.default.js
cp ./scripts/next.config.build.js ./next.config.js
sudo docker compose -f docker/development/compose.yaml down
sudo docker compose -f docker/development/compose.yaml build
sudo docker compose -f docker/development/compose.yaml up -d
cp ./scripts/next.config.default.js ./next.config.js

4
src/app/api/auth/[...nextauth]/route.ts Normal file → Executable file
View File

@ -1,2 +1,2 @@
import { handlers } from "~/auth" import { handlers } from '~/auth';
export const { GET, POST } = handlers export const { GET, POST } = handlers;

View File

@ -0,0 +1,27 @@
'use server';
import { NextResponse } from 'next/server';
import { get_history } from '~/server/functions';
import { auth } from '~/auth';
export const GET = async (request: Request) => {
try {
const url = new URL(request.url);
const apiKey = url.searchParams.get('apikey');
const userId = Number(url.searchParams.get('user_id')) || -1;
const page = Number(url.searchParams.get('page')) || 1;
const perPage = Number(url.searchParams.get('per_page')) || 50;
if (apiKey !== process.env.API_KEY) {
const session = await auth();
if (!session)
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
const historyData = await get_history(userId, page, perPage);
return NextResponse.json(historyData, { status: 200 });
} catch (error) {
console.error('Error fetching history data:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 },
);
}
};

View File

@ -1,14 +1,8 @@
"use server"; 'use server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getEmployees } from '~/server/functions'; import { getEmployees } from '~/server/functions';
import { auth } from '~/auth'; import { auth } from '~/auth';
type Technician = {
name: string;
status: string;
updatedAt: Date;
};
export const GET = async (request: Request) => { export const GET = async (request: Request) => {
try { try {
const session = await auth(); const session = await auth();
@ -16,18 +10,10 @@ export const GET = async (request: Request) => {
const url = new URL(request.url); const url = new URL(request.url);
const apiKey = url.searchParams.get('apikey'); const apiKey = url.searchParams.get('apikey');
if (apiKey !== process.env.API_KEY) if (apiKey !== process.env.API_KEY)
return NextResponse.json( return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
{ message: 'Unauthorized' },
{ status: 401 }
);
else { else {
const employees = await getEmployees(); const employees = await getEmployees();
const formattedEmployees = employees.map((employee: Technician) => ({ return NextResponse.json(employees, { status: 200 });
name: employee.name,
status: employee.status,
time: employee.updatedAt
}));
return NextResponse.json(formattedEmployees, { status: 200 });
} }
} else { } else {
const employees = await getEmployees(); const employees = await getEmployees();
@ -37,7 +23,7 @@ export const GET = async (request: Request) => {
console.error('Error fetching employees:', error); console.error('Error fetching employees:', error);
return NextResponse.json( return NextResponse.json(
{ message: 'Internal server error' }, { message: 'Internal server error' },
{ status: 500 } { status: 500 },
); );
} }
}; };

View File

@ -1,25 +0,0 @@
"use server";
import { NextResponse } from 'next/server';
import { getHistory } from '~/server/functions';
export const GET = async (request: Request) => {
try {
const url = new URL(request.url);
const apiKey = url.searchParams.get('apikey');
const page = Number(url.searchParams.get('page')) || 1;
if (apiKey !== process.env.API_KEY)
return NextResponse.json(
{ message: 'Unauthorized' },
{ status: 401 }
);
const perPage = 50;
const historyData = await getHistory(page, perPage);
return NextResponse.json(historyData, { status: 200 });
} catch (error) {
console.error('Error fetching history data:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
};

52
src/app/api/update_status_by_id/route.ts Normal file → Executable file
View File

@ -1,5 +1,5 @@
// Update Employee Status by IDs // Update Employee Status by IDs
"use server"; 'use server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { updateEmployeeStatus } from '~/server/functions'; import { updateEmployeeStatus } from '~/server/functions';
@ -12,33 +12,27 @@ type UpdateStatusBody = {
export const POST = async (req: NextRequest) => { export const POST = async (req: NextRequest) => {
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
const url = new URL(req.url); const url = new URL(req.url);
const apiKey = url.searchParams.get('apikey'); const apiKey = url.searchParams.get('apikey');
if (apiKey !== process.env.API_KEY) if (apiKey !== process.env.API_KEY)
return NextResponse.json( return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
{ message: 'Unauthorized' }, } else {
{ status: 401 } const { employeeIds, newStatus } = (await req.json()) as UpdateStatusBody;
); if (!Array.isArray(employeeIds) || typeof newStatus !== 'string')
} else { return NextResponse.json({ message: 'Invalid input' }, { status: 400 });
const { employeeIds, newStatus } = await req.json() as UpdateStatusBody; try {
if (!Array.isArray(employeeIds) || typeof newStatus !== 'string') await updateEmployeeStatus(employeeIds, newStatus);
return NextResponse.json( return NextResponse.json(
{ message: 'Invalid input' }, { message: 'Status updated successfully' },
{ status: 400 } { status: 200 },
); );
try { } catch (error) {
await updateEmployeeStatus(employeeIds, newStatus); console.error('Error updating status:', error);
return NextResponse.json( return NextResponse.json(
{ message: 'Status updated successfully' }, { message: 'Internal server error' },
{ status: 200 } { status: 500 },
); );
} catch (error) {
console.error('Error updating status:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
} }
}
}; };

View File

@ -1,20 +1,22 @@
// Update Employee Status by Names // Update Employee Status by Names
"use server"; 'use server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { updateEmployeeStatusByName } from '~/server/functions'; import { updateEmployeeStatusByName } from '~/server/functions';
type Technician = { type Technician = {
name: string; name: string;
status: string; status: string;
} };
// Type guard to check if an object is a Technician // Type guard to check if an object is a Technician
const isTechnician = (technician: unknown): technician is Technician => { const isTechnician = (technician: unknown): technician is Technician => {
if (typeof technician !== 'object' || technician === null) return false; if (typeof technician !== 'object' || technician === null) return false;
return 'name' in technician && return (
'name' in technician &&
typeof (technician as Technician).name === 'string' && typeof (technician as Technician).name === 'string' &&
'status' in technician && 'status' in technician &&
typeof (technician as Technician).status === 'string'; typeof (technician as Technician).status === 'string'
);
}; };
export const POST = async (request: Request) => { export const POST = async (request: Request) => {
@ -25,28 +27,31 @@ export const POST = async (request: Request) => {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
const body: unknown = await request.json(); const body: unknown = await request.json();
// Validate the body and its technicians property // Validate the body and its technicians property
if (typeof body !== 'object' || body === null || if (
!Array.isArray((body as { technicians?: unknown[] }).technicians)) typeof body !== 'object' ||
body === null ||
!Array.isArray((body as { technicians?: unknown[] }).technicians)
)
return NextResponse.json( return NextResponse.json(
{ message: 'Invalid input: expecting an array of technicians.' }, { message: 'Invalid input: expecting an array of technicians.' },
{ status: 400 } { status: 400 },
); );
const technicians = (body as { technicians: unknown[] }).technicians; const technicians = (body as { technicians: unknown[] }).technicians;
if (!technicians.every(isTechnician)) if (!technicians.every(isTechnician))
return NextResponse.json( return NextResponse.json(
{ message: 'Invalid input: missing name or status for a technician.' }, { message: 'Invalid input: missing name or status for a technician.' },
{ status: 400 } { status: 400 },
); );
await updateEmployeeStatusByName(technicians); await updateEmployeeStatusByName(technicians);
return NextResponse.json( return NextResponse.json(
{ message: 'Technicians updated successfully.' }, { message: 'Technicians updated successfully.' },
{ status: 200 } { status: 200 },
); );
} catch (error) { } catch (error) {
console.error('Error updating technicians:', error); console.error('Error updating technicians:', error);
return NextResponse.json( return NextResponse.json(
{ message: 'Internal server error' }, { message: 'Internal server error' },
{ status: 500 } { status: 500 },
); );
} }
}; };

34
src/app/layout.tsx Normal file → Executable file
View File

@ -1,14 +1,15 @@
import "~/styles/globals.css"; import '~/styles/globals.css';
import { Inter as FontSans } from "next/font/google"; import { Inter as FontSans } from 'next/font/google';
import { cn } from "~/lib/utils"; import { cn } from '~/lib/utils';
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from 'next-auth/react';
import { TVModeProvider } from "~/components/context/TVModeContext"; import { TVModeProvider } from '~/components/context/TVModeContext';
import { type Metadata } from "next"; import { type Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tech Tracker", title: 'Tech Tracker',
description: "App used by COG IT employees to \ description:
update their status throughout the day.", 'App used by COG IT employees to \
update their status throughout the day.',
icons: [ icons: [
{ {
rel: 'icon', rel: 'icon',
@ -28,24 +29,23 @@ export const metadata: Metadata = {
}; };
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ['latin'],
variable: "--font-sans", variable: '--font-sans',
}); });
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
return ( return (
<html lang="en"> <html lang='en'>
<body <body
className={cn( className={cn(
"min-h-screen bg-background font-sans antialiased", 'min-h-screen bg-background font-sans antialiased',
fontSans.variable)} fontSans.variable,
)}
> >
<SessionProvider> <SessionProvider>
<TVModeProvider> <TVModeProvider>{children}</TVModeProvider>
{children}
</TVModeProvider>
</SessionProvider> </SessionProvider>
</body> </body>
</html> </html>

24
src/app/page.tsx Normal file → Executable file
View File

@ -1,21 +1,23 @@
"use server"; 'use server';
import { auth } from "~/auth"; import { auth } from '~/auth';
import No_Session from "~/components/ui/No_Session"; import No_Session from '~/components/ui/No_Session';
import Header from "~/components/ui/Header"; import Header from '~/components/ui/Header';
import { getEmployees } from "~/server/functions"; import { getEmployees } from '~/server/functions';
import Tech_Table from "~/components/ui/Tech_Table"; import Tech_Table from '~/components/ui/Tech_Table';
export default async function HomePage() { export default async function HomePage() {
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
return <No_Session /> return <No_Session />;
} else { } else {
const employees = await getEmployees(); const employees = await getEmployees();
return ( return (
<main className="min-h-screen <main
bg-gradient-to-b from-[#111111] to-[#212325]"> className='min-h-screen
<Header /> bg-gradient-to-b from-[#111111] to-[#212325]'
<Tech_Table employees={employees}/> >
<Header />
<Tech_Table employees={employees} />
</main> </main>
); );
} }

14
src/auth.ts Normal file → Executable file
View File

@ -1,6 +1,7 @@
import NextAuth from "next-auth" import NextAuth from 'next-auth';
import Entra from "next-auth/providers/microsoft-entra-id" import Entra from 'next-auth/providers/microsoft-entra-id';
import Authentik from 'next-auth/providers/authentik';
export const { handlers, auth, signIn, signOut } = NextAuth({ export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [ providers: [
Entra({ Entra({
@ -8,5 +9,10 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
tenantId: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID, tenantId: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID,
}), }),
Authentik({
clientId: process.env.AUTH_AUTHENTIK_CLIENT_ID,
clientSecret: process.env.AUTH_AUTHENTIK_CLIENT_SECRET,
issuer: process.env.AUTH_AUTHENTIK_ISSUER,
}),
], ],
}) });

38
src/components/auth/client/Sign_Out.tsx Normal file → Executable file
View File

@ -1,6 +1,6 @@
import { signOut } from "next-auth/react"; import { signOut } from 'next-auth/react';
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
import Image from "next/image"; import Image from 'next/image';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -8,33 +8,35 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "~/components/ui/shadcn/dropdown-menu"; } from '~/components/ui/shadcn/dropdown-menu';
export default function Sign_Out() { export default function Sign_Out() {
const { data: session } = useSession(); const { data: session } = useSession();
if (!session) { if (!session) {
return <div/>; return <div />;
} else { } else {
const pfp = session?.user?.image ? session.user.image : "/images/default_user_pfp.png"; const pfp = session?.user?.image
const name = session?.user?.name ? session.user.name : "Profile"; ? session.user.image
: '/images/default_user_pfp.png';
const name = session?.user?.name ? session.user.name : 'Profile';
return ( return (
<div className="m-auto mt-1"> <div className='m-auto mt-1'>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Image src={pfp} alt="" width={35} height={35} <Image
className="rounded-full border-2 border-white m-auto mr-1 md:mr-2 src={pfp}
max-w-[25px] md:max-w-[35px]" alt=''
width={35}
height={35}
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2
max-w-[25px] md:max-w-[35px]'
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel> <DropdownMenuLabel>{name}</DropdownMenuLabel>
{name}
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem>
<button onClick={() => signOut()} <button onClick={() => signOut()} className='w-full'>
className="w-full"
>
Sign Out Sign Out
</button> </button>
</DropdownMenuItem> </DropdownMenuItem>
@ -43,4 +45,4 @@ export default function Sign_Out() {
</div> </div>
); );
} }
}; }

View File

@ -0,0 +1,15 @@
import { signIn } from 'next-auth/react';
import { Button } from '~/components/ui/shadcn/button';
export default function Sign_In_Authentik() {
return (
<Button
onClick={() => signIn('authentik')}
className='bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl
px-4 py-2 md:py-2.5 font-semibold text-white hover:bg-gradient-to-tr
hover:from-[#35363F] hover:to-[#23242F]'
>
<h1 className='md:text-2xl my-auto font-semibold'>Sign In</h1>
</Button>
);
}

View File

@ -0,0 +1,15 @@
import { signIn } from 'next-auth/react';
import { Button } from '~/components/ui/shadcn/button';
export default function Sign_In_Microsoft() {
return (
<Button
onClick={() => signIn('microsoft-entra-id')}
className='bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl
px-4 py-2 md:py-2.5 font-semibold text-white hover:bg-gradient-to-tr
hover:from-[#35363F] hover:to-[#23242F]'
>
<h1 className='md:text-2xl my-auto font-semibold'>Sign In</h1>
</Button>
);
}

42
src/components/auth/server/Sign_Out.tsx Normal file → Executable file
View File

@ -1,31 +1,41 @@
import Image from "next/image"; import Image from 'next/image';
import { auth } from "~/auth" import { auth } from '~/auth';
import { signOut } from "~/auth" import { signOut } from '~/auth';
export default async function Sign_Out() { export default async function Sign_Out() {
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
return (<div/>); return <div />;
} else { } else {
// Add User profile picture next to Sign Out button // Add User profile picture next to Sign Out button
const pfp = session?.user?.image ? session.user.image : "/images/default_user_pfp.png"; const pfp = session?.user?.image
? session.user.image
: '/images/default_user_pfp.png';
return ( return (
<form className="flex flex-row" <form
className='flex flex-row'
action={async () => { action={async () => {
"use server" 'use server';
await signOut() await signOut();
}}> }}
<Image src={pfp} alt="" width={35} height={35} >
className="rounded-full border-2 border-white m-auto mr-1 md:mr-2 <Image
max-w-[25px] md:max-w-[35px]" src={pfp}
alt=''
width={35}
height={35}
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2
max-w-[25px] md:max-w-[35px]'
/> />
<button type="submit" className="w-full p-2 rounded-xl text-sm md:text-lg <button
type='submit'
className='w-full p-2 rounded-xl text-sm md:text-lg
bg-gradient-to-tl from-[#35363F] to=[#24191A] bg-gradient-to-tl from-[#35363F] to=[#24191A]
hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]" hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]'
> >
Sign Out Sign Out
</button> </button>
</form> </form>
); );
} }
}; }

View File

@ -0,0 +1,32 @@
import Image from 'next/image';
import { signIn } from '~/auth';
const Sign_In_Authentik = () => {
return (
<form
className='items-center justify-center mx-auto'
action={async () => {
'use server';
await signIn('authentik');
}}
>
<button
type='submit'
className='flex flex-col mx-auto text-center items-center
bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl px-4 py-2 md:py-2.5
font-semibold text-white hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]'
>
<h1 className='md:text-2xl my-auto font-semibold mb-1'>
Sign In with
</h1>
<Image
src='/images/authentik_logo.svg'
alt='Microsoft'
width={150}
height={150}
/>
</button>
</form>
);
};
export default Sign_In_Authentik;

View File

@ -0,0 +1,30 @@
import Image from 'next/image';
import { signIn } from '~/auth';
export default async function Sign_In_Microsoft() {
return (
<form
className='items-center justify-center mx-auto'
action={async () => {
'use server';
await signIn('microsoft-entra-id');
}}
>
<button
type='submit'
className='flex flex-col mx-auto text-center items-center
bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl px-4 py-2 md:py-2.5
font-semibold text-white hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]'
>
<h1 className='md:text-2xl my-auto font-semibold'>Sign In with</h1>
<Image
src='/images/microsoft_logo.svg'
alt='Microsoft'
width={150}
height={150}
className='ml-2'
/>
</button>
</form>
);
}

4
src/components/context/TVModeContext.tsx Normal file → Executable file
View File

@ -1,4 +1,4 @@
"use client"; 'use client';
import React, { createContext, useContext, useState } from 'react'; import React, { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@ -11,7 +11,7 @@ const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
export const TVModeProvider = ({ children }: { children: ReactNode }) => { export const TVModeProvider = ({ children }: { children: ReactNode }) => {
const [tvMode, setTVMode] = useState(false); const [tvMode, setTVMode] = useState(false);
const toggleTVMode = () => { const toggleTVMode = () => {
setTVMode((prev) => !prev); setTVMode((prev) => !prev);
}; };

49
src/components/ui/Header.tsx Normal file → Executable file
View File

@ -1,41 +1,48 @@
"use client"; 'use client';
import Image from "next/image"; import Image from 'next/image';
import Sign_Out from "~/components/auth/client/Sign_Out"; import Sign_Out from '~/components/auth/client/Sign_Out';
import TV_Toggle from "~/components/ui/TV_Toggle"; import TV_Toggle from '~/components/ui/TV_Toggle';
import { useTVMode } from "~/components/context/TVModeContext"; import { useTVMode } from '~/components/context/TVModeContext';
export default function Header() { export default function Header() {
const { tvMode } = useTVMode(); const { tvMode } = useTVMode();
if (tvMode) { if (tvMode) {
return ( return (
<div className="absolute top-4 right-2"> <div className='absolute top-4 right-2'>
<div className="flex flex-row my-auto items-center pt-2 pr-0 md:pt-4"> <div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4'>
< TV_Toggle /> <TV_Toggle />
</div> </div>
</div> </div>
); );
} else { } else {
return ( return (
<header className="w-full py-2 pt-6 md:py-5"> <header className='w-full py-2 pt-6 md:py-5'>
<div className="absolute top-4 right-6"> <div className='absolute top-4 right-6'>
<div className="flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8"> <div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8'>
< TV_Toggle /> <TV_Toggle />
< Sign_Out /> <Sign_Out />
</div> </div>
</div> </div>
<div className="flex flex-row items-center text-center <div
sm:justify-center ml-4 sm:ml-0 p-4"> className='flex flex-row items-center text-center
<Image src="/images/tech_tracker_logo.png" justify-center sm:ml-0 p-4 mt-10 sm:mt-0'
alt="Tech Tracker Logo" width={100} height={100} >
className="max-w-[40px] md:max-w-[120px]" <Image
src='/images/tech_tracker_logo.png'
alt='Tech Tracker Logo'
width={100}
height={100}
className='max-w-[40px] md:max-w-[120px]'
/> />
<h1 className="title-text text-sm md:text-4xl lg:text-8xl <h1
className='title-text text-sm md:text-4xl lg:text-8xl
bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7] bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7]
font-bold pl-2 md:pl-12 text-transparent bg-clip-text"> font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
>
Tech Tracker Tech Tracker
</h1> </h1>
</div> </div>
</header> </header>
); );
} }
}; }

208
src/components/ui/History_Drawer.tsx Normal file → Executable file
View File

@ -1,13 +1,13 @@
import Image from "next/image"; import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import { ScrollArea } from '~/components/ui/shadcn/scroll-area';
import { import {
Drawer,
DrawerTrigger,
DrawerClose, DrawerClose,
DrawerContent, DrawerContent,
DrawerFooter, DrawerFooter,
DrawerHeader, DrawerHeader,
DrawerTitle, DrawerTitle,
} from "~/components/ui/shadcn/drawer"; } from '~/components/ui/shadcn/drawer';
import { import {
Table, Table,
TableBody, TableBody,
@ -15,78 +15,168 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/shadcn/table"; } from '~/components/ui/shadcn/table';
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink, PaginationLink,
PaginationItem,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} from "~/components/ui/shadcn/pagination"; } from '~/components/ui/shadcn/pagination';
// Type definitions for Paginated History API
type HistoryEntry = {
name: string;
status: string;
updatedAt: Date;
};
type PaginatedHistory = {
data: HistoryEntry[];
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
type History_Drawer_Props = {
user_id: number;
};
const History_Drawer: React.FC<History_Drawer_Props> = ({ user_id }) => {
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [page, setPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState<number>(1);
const perPage = 50;
const fetchHistory = async (currentPage: number, user_id: number) => {
try {
const response = await fetch(
`/api/get_paginated_history?user_id=${user_id}&page=${currentPage}&per_page=${perPage}`,
);
if (!response.ok) throw new Error('Failed to fetch history');
const data: PaginatedHistory =
(await response.json()) as PaginatedHistory;
setHistory(data.data);
setTotalPages(data.meta.total_pages);
} catch (error) {
console.error('Error fetching history:', error);
}
};
useEffect(() => {
fetchHistory(page, user_id).catch((error) => {
console.error('Error fetching history:', error);
});
}, [page, user_id]);
const handlePageChange = (newPage: number) => {
setPage(newPage);
};
export default function History_Drawer() {
return ( return (
<Drawer> <DrawerContent>
<DrawerTrigger> <DrawerHeader>
Status <DrawerTitle>
</DrawerTrigger> <div className='flex flex-row items-center text-center sm:justify-center sm:ml-0 py-4'>
<DrawerContent> <Image
<DrawerHeader> src='/images/tech_tracker_logo.png'
<DrawerTitle> alt='Tech Tracker Logo'
<div className="flex flex-row items-center text-center width={60}
sm:justify-center sm:ml-0 py-4"> height={60}
<Image src="/images/tech_tracker_logo.png" className='max-w-[40px] md:max-w-[120px]'
alt="Tech Tracker Logo" width={60} height={60} />
className="max-w-[40px] md:max-w-[120px]" <h1 className='title-text text-sm md:text-2xl lg:text-6xl bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7] font-bold pl-2 md:pl-4 text-transparent bg-clip-text'>
/> History
<h1 className="title-text text-sm md:text-2xl lg:text-6xl </h1>
bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7] </div>
font-bold pl-2 md:pl-4 text-transparent bg-clip-text"> </DrawerTitle>
History </DrawerHeader>
</h1> <ScrollArea className='w-full sm:w-5/6 lg:w-5/12 m-auto h-80'>
</div> <Table className='w-full m-auto'>
</DrawerTitle>
</DrawerHeader>
<Table className="w-5/6 lg:w-1/2 m-auto"
>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="">Name</TableHead> <TableHead className='font-semibold lg:max-w-[100px]'>
<TableHead>Status</TableHead> Name
<TableHead>Updated At</TableHead> </TableHead>
<TableHead className='font-semibold lg:max-w-[100px]'>
Status
</TableHead>
<TableHead className='font-semibold lg:max-w-[100px] justify-end items-end text-right'>
Updated At
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow> {history.map((entry, index) => (
<TableCell className="font-medium">INV001</TableCell> <TableRow key={index}>
<TableCell>Paid</TableCell> <TableCell className='font-medium lg:max-w-[100px]'>
<TableCell>Credit Card</TableCell> {entry.name}
</TableRow> </TableCell>
<TableCell className='font-medium lg:max-w-[100px] wrapword'>
{entry.status}
</TableCell>
<TableCell className='font-medium lg:max-w-[100px] justify-end items-end text-right'>
{new Date(entry.updatedAt).toLocaleString()}
</TableCell>
</TableRow>
))}
</TableBody> </TableBody>
</Table> </Table>
<DrawerFooter> </ScrollArea>
<Pagination> <DrawerFooter>
<PaginationContent> <Pagination>
<PaginationContent>
{page > 1 && (
<PaginationItem> <PaginationItem>
<PaginationPrevious href="#" /> <PaginationPrevious
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page - 1);
}}
/>
</PaginationItem> </PaginationItem>
)}
{totalPages > 10 && (
<h3 className='text-center flex flex-row'>
Page
<h3 className='font-bold mx-1'>{page}</h3>
of
<h3 className='font-semibold ml-1'>{totalPages}</h3>
</h3>
)}
{totalPages <= 10 &&
Array.from({ length: totalPages }).map((_, idx) => (
<PaginationItem key={idx}>
<PaginationLink
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(idx + 1);
}}
>
{idx + 1}
</PaginationLink>
</PaginationItem>
))}
{page < totalPages && (
<PaginationItem> <PaginationItem>
<PaginationLink href="#">1</PaginationLink> <PaginationNext
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page + 1);
}}
/>
</PaginationItem> </PaginationItem>
<PaginationItem> )}
<PaginationEllipsis /> </PaginationContent>
</PaginationItem> </Pagination>
<PaginationItem> <DrawerClose></DrawerClose>
<PaginationNext href="#" /> </DrawerFooter>
</PaginationItem> </DrawerContent>
</PaginationContent> );
</Pagination>
<DrawerClose>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}; };
export default History_Drawer;

47
src/components/ui/Loading.tsx Normal file → Executable file
View File

@ -1,30 +1,29 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Progress } from "~/components/ui/shadcn/progress" import { Progress } from '~/components/ui/shadcn/progress';
interface Loading_Props { interface Loading_Props {
interval_amount: number interval_amount: number;
} }
const Loading: React.FC<Loading_Props> = ({interval_amount}) => { const Loading: React.FC<Loading_Props> = ({ interval_amount }) => {
const [progress, setProgress] = React.useState(13); const [progress, setProgress] = React.useState(13);
React.useEffect(() => { React.useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setProgress((prev) => { setProgress((prev) => {
if (prev >= 100) { if (prev >= 100) {
clearInterval(interval); clearInterval(interval);
return 0; return 0;
} }
return prev + interval_amount; return prev + interval_amount;
}); });
}, 50); }, 50);
return () => clearInterval(interval); return () => clearInterval(interval);
});
}) return (
return ( <div className='items-center justify-center w-1/3 m-auto pt-20'>
<div className="items-center justify-center w-1/3 m-auto pt-20"> <Progress value={progress} />
<Progress value={progress} /> </div>
</div> );
); };
}
export default Loading; export default Loading;

64
src/components/ui/No_Session.tsx Normal file → Executable file
View File

@ -1,31 +1,49 @@
import Link from "next/link"; import Link from 'next/link';
import Image from "next/image"; import Image from 'next/image';
import Sign_In from "~/components/auth/server/Sign_In"; import Sign_In_Microsoft from '~/components/auth/server/microsoft/Sign_In';
import Header from "~/components/ui/Header"; import Sign_In_Authentik from '~/components/auth/server/authentik/Sign_In';
import Header from '~/components/ui/Header';
export default function No_Session() { export default function No_Session() {
return ( return (
<main className="w-full min-h-screen mx-auto text-center pt-2 md:pt-10 <main
bg-gradient-to-b from-[#111111] to-[#212325]"> className='w-full min-h-screen mx-auto text-center pt-2 md:pt-10
<div className="md:w-2/3 pt-4 pb-2 md:pt-10 md:pb-4 m-auto"> bg-gradient-to-b from-[#111111] to-[#212325]'
< Header /> >
<div className='md:w-2/3 pt-4 pb-2 md:pt-10 md:pb-4 m-auto'>
<Header />
</div> </div>
<div className="mx-auto flex flex-col"> <div className='mx-auto flex flex-col items-center justify-center'>
<div className="py-4"> <div className='flex sm:flex-row flex-col p-4'>
< Sign_In /> <div className='p-4'>
<Sign_In_Microsoft />
</div>
<div className='p-4'>
<Sign_In_Authentik />
</div>
</div> </div>
<Link href="https://git.gibbyb.com/gib/Tech_Tracker_Web" </div>
className="text-center text-[16px] md:text-lg px-4 py-2 md:py-2.5 font-semibold <div className='sm:absolute sm:bottom-6 sm:left-4 flex flex-row'>
bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl hover:text-sky-200 <Link
hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F] href='https://git.gibbyb.com/gib/Tech_Tracker_Web'
mx-auto flex flex-row" className='text-center text-[16px] md:text-lg p-2 font-semibold
bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl hover:text-sky-200
hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]
mx-auto flex flex-col items-center justify-center'
> >
<Image src="/images/gitea_logo.svg" alt="Gitea" width={35} height={35} <h3 className='my-auto'>View Source Code on</h3>
className="mr-2" <div className='flex flex-row px-2'>
/> <Image
<h3 className="my-auto">Source Code</h3> src='/images/gitea_logo.svg'
</Link> alt='Gitea'
</div> width={40}
height={40}
className='mr-2'
/>
<h3 className='my-auto text-2xl'>Gitea</h3>
</div>
</Link>
</div>
</main> </main>
); );
}; }

26
src/components/ui/TV_Toggle.tsx Normal file → Executable file
View File

@ -1,21 +1,27 @@
"use client"; 'use client';
import Image from "next/image"; import Image from 'next/image';
import { useTVMode } from "~/components/context/TVModeContext"; import { useTVMode } from '~/components/context/TVModeContext';
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
export default function TV_Toggle() { export default function TV_Toggle() {
const { tvMode, toggleTVMode } = useTVMode(); const { tvMode, toggleTVMode } = useTVMode();
const { data: session } = useSession(); const { data: session } = useSession();
if (!session) return <div/>; if (!session) return <div />;
return ( return (
<button onClick={toggleTVMode} className="mr-4 mt-1"> <button onClick={toggleTVMode} className='mr-4 mt-1'>
{tvMode ? ( {tvMode ? (
<Image src="/images/exit_fullscreen.svg" alt="Exit TV Mode" <Image
width={25} height={25} src='/images/exit_fullscreen.svg'
alt='Exit TV Mode'
width={25}
height={25}
/> />
) : ( ) : (
<Image src="/images/fullscreen.svg" alt="Enter TV Mode" <Image
width={25} height={25} src='/images/fullscreen.svg'
alt='Enter TV Mode'
width={25}
height={25}
/> />
)} )}
</button> </button>

158
src/components/ui/Tech_Table.tsx Normal file → Executable file
View File

@ -1,16 +1,19 @@
"use client"; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
import Loading from "~/components/ui/Loading"; import Loading from '~/components/ui/Loading';
import { useTVMode } from "~/components/context/TVModeContext"; import { useTVMode } from '~/components/context/TVModeContext';
import History_Drawer from "~/components/ui/History_Drawer"; import { Drawer, DrawerTrigger } from '~/components/ui/shadcn/drawer';
import { ScrollArea } from '~/components/ui/shadcn/scroll-area';
import History_Drawer from '~/components/ui/History_Drawer';
type Employee = { type Employee = {
id: number; id: number;
name: string; name: string;
status: string; status: string;
updatedAt: Date; updatedAt: Date;
} };
export default function Tech_Table({ employees }: { employees: Employee[] }) { export default function Tech_Table({ employees }: { employees: Employee[] }) {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
@ -20,34 +23,40 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
const [selectAll, setSelectAll] = useState(false); const [selectAll, setSelectAll] = useState(false);
const [employeeStatus, setStatus] = useState(''); const [employeeStatus, setStatus] = useState('');
const [employeeData, setEmployeeData] = useState(employees); const [employeeData, setEmployeeData] = useState(employees);
const [selectedUserId, setSelectedUserId] = useState(-1);
const fetch_employees = useCallback(async (): Promise<Employee[]> => { const fetch_employees = useCallback(async (): Promise<Employee[]> => {
const res = await fetch('/api/technicians', { const res = await fetch('/api/get_technicians', {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${process.env.API_KEY}` Authorization: `Bearer ${process.env.API_KEY}`,
} },
}); });
return res.json() as Promise<Employee[]>; return res.json() as Promise<Employee[]>;
}, []); }, []);
const update_status = async () => { const update_status = async () => {
if (!session) { if (!session) {
alert("You must be signed in to update status."); alert('You must be signed in to update status.');
return; return;
} else if (selectedIds.length === 0 && employeeStatus.trim() !== '') { } else if (selectedIds.length === 0 && employeeStatus.trim() !== '') {
const users_name = session.user?.name ?? ""; const users_name = session.user?.name ?? '';
const name_arr = users_name.split(' '); const name_arr = users_name.split(' ');
const lname = name_arr[name_arr.length - 1] ?? ""; const lname = name_arr[name_arr.length - 1] ?? '';
const cur_user = employees.find(employee => employee.name.includes(lname)); const cur_user = employees.find((employee) =>
employee.name.includes(lname),
);
if (cur_user) { if (cur_user) {
await fetch('/api/update_status_by_id', { await fetch('/api/update_status_by_id', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.API_KEY}` Authorization: `Bearer ${process.env.API_KEY}`,
}, },
body: JSON.stringify({ employeeIds: [cur_user.id], newStatus: employeeStatus }), body: JSON.stringify({
employeeIds: [cur_user.id],
newStatus: employeeStatus,
}),
}); });
} }
} else if (employeeStatus.trim() !== '') { } else if (employeeStatus.trim() !== '') {
@ -55,12 +64,15 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.API_KEY}` Authorization: `Bearer ${process.env.API_KEY}`,
}, },
body: JSON.stringify({ employeeIds: selectedIds, newStatus: employeeStatus }), body: JSON.stringify({
employeeIds: selectedIds,
newStatus: employeeStatus,
}),
}); });
} }
const updatedEmployees = await fetch_employees(); const updatedEmployees = await fetch_employees();
setEmployeeData(updatedEmployees); setEmployeeData(updatedEmployees);
setSelectedIds([]); setSelectedIds([]);
@ -71,7 +83,7 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
setSelectedIds((prevSelected) => setSelectedIds((prevSelected) =>
prevSelected.includes(id) prevSelected.includes(id)
? prevSelected.filter((prevId) => prevId !== id) ? prevSelected.filter((prevId) => prevId !== id)
: [...prevSelected, id] : [...prevSelected, id],
); );
}; };
@ -95,6 +107,10 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
} }
}; };
const handleStatusClick = (id: number) => {
setSelectedUserId(id);
};
const formatTime = (timestamp: Date) => { const formatTime = (timestamp: Date) => {
const date = new Date(timestamp); const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', { const time = date.toLocaleTimeString('en-US', {
@ -107,7 +123,7 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
}; };
useEffect(() => { useEffect(() => {
if (status !== "loading") { if (status !== 'loading') {
setLoading(false); setLoading(false);
} }
}, [status]); }, [status]);
@ -121,7 +137,7 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
const updatedEmployees = await fetch_employees(); const updatedEmployees = await fetch_employees();
setEmployeeData(updatedEmployees); setEmployeeData(updatedEmployees);
}; };
fetchAndUpdateEmployees().catch((error) => { fetchAndUpdateEmployees().catch((error) => {
console.error('Error fetching employees:', error); console.error('Error fetching employees:', error);
}); });
@ -133,7 +149,7 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
console.error('Error fetching employees:', error); console.error('Error fetching employees:', error);
}); });
}, 10000); }, 10000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [fetch_employees]); }, [fetch_employees]);
@ -149,55 +165,77 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
else { else {
return ( return (
<div className={`${tvMode ? 'content-fullscreen' : ''}`}> <div className={`${tvMode ? 'content-fullscreen' : ''}`}>
{tvMode && <div className="w-full tablefill"></div>} {tvMode && <div className='w-full tablefill'></div>}
<table className={`techtable m-auto text-center text-[42px] <table
${tvMode ? 'techtable-fullscreen' : 'w-5/6'}`}> className={`techtable m-auto text-center text-[42px]
<thead className="tabletitles border border-[#3e4446] bg-gradient-to-b ${tvMode ? 'techtable-fullscreen' : 'w-5/6'}`}
from-[#282828] to-[#383838] text-[48px]"> >
<thead
className='tabletitles border border-[#3e4446] bg-gradient-to-b
from-[#282828] to-[#383838] text-[48px]'
>
<tr> <tr>
{!tvMode && ( {!tvMode && (
<th className="py-3 px-6 border border-[#3e4446]"> <th className='py-3 px-3 md:px-6 border border-[#3e4446]'>
<input <input
type="checkbox" type='checkbox'
className="m-auto cursor-pointer scale-150" className='m-auto cursor-pointer md:scale-150'
checked={selectAll} checked={selectAll}
onChange={handleSelectAllChange} onChange={handleSelectAllChange}
/> />
</th> </th>
)} )}
<th className="border border-[#3e4446] py-3">Name</th> <th className='border border-[#3e4446] py-3'>Name</th>
<th className="border border-[#3e4446] py-3"> <th className='border border-[#3e4446] py-3'>
<History_Drawer /> <Drawer>
<DrawerTrigger>Status</DrawerTrigger>
<History_Drawer user_id={-1} />
</Drawer>
</th> </th>
<th className="border border-[#3e4446] py-3">Updated At</th> <th className='border border-[#3e4446] py-3'>Updated At</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{employeeData.map((employee) => ( {employeeData.map((employee) => (
<tr <tr
className="even:bg-gradient-to-br from-[#272727] to-[#313131] className='even:bg-gradient-to-br from-[#272727] to-[#313131]
odd:bg-gradient-to-bl odd:from-[#252525] odd:to-[#212125]" odd:bg-gradient-to-bl odd:from-[#252525] odd:to-[#212125]'
key={employee.id} key={employee.id}
> >
{!tvMode && ( {!tvMode && (
<td className="p-1 border border-[#3e4446]"> <td className='p-1 border border-[#3e4446]'>
<input <input
type="checkbox" type='checkbox'
className="m-auto cursor-pointer scale-150" className='m-auto cursor-pointer md:scale-150'
checked={selectedIds.includes(employee.id)} checked={selectedIds.includes(employee.id)}
onChange={() => handleCheckboxChange(employee.id)} onChange={() => handleCheckboxChange(employee.id)}
/> />
</td> </td>
)} )}
<td className="n-column px-1 md:py-3 border border-[#3e4446]"> <td className='n-column px-1 md:py-3 border border-[#3e4446]'>
{employee.name} {employee.name}
</td> </td>
<td className="s-column max-w-[700px] px-1 md:py-3 border border-[#3e4446]"> <td
<button> className='s-column max-w-[700px] px-1 md:py-3 border
{employee.status} border-[#3e4446] wrapword max-h-0'
</button> >
<ScrollArea className='w-full m-auto h-[60px]'>
<Drawer>
<DrawerTrigger>
<button onClick={() => handleStatusClick(employee.id)}>
{employee.status}
</button>
</DrawerTrigger>
{selectedUserId !== -1 && (
<History_Drawer
key={selectedUserId}
user_id={selectedUserId}
/>
)}
</Drawer>
</ScrollArea>
</td> </td>
<td className="ua-column px-1 md:py-3 border border-[#3e4446]"> <td className='ua-column px-1 md:py-3 border border-[#3e4446]'>
{formatTime(employee.updatedAt)} {formatTime(employee.updatedAt)}
</td> </td>
</tr> </tr>
@ -205,23 +243,23 @@ export default function Tech_Table({ employees }: { employees: Employee[] }) {
</tbody> </tbody>
</table> </table>
{!tvMode && ( {!tvMode && (
<div className="m-auto flex flex-row items-center justify-center py-5"> <div className='m-auto flex flex-row items-center justify-center py-5'>
<input <input
autoFocus autoFocus
type="text" type='text'
placeholder="New Status" placeholder='New Status'
className="min-w-[120px] lg:min-w-[400px] bg-[#F9F6EE] py-2 px-3 className='min-w-[120px] lg:min-w-[400px] bg-[#F9F6EE] py-2 px-3
border-none rounded-xl text-[#111111] lg:text-2xl" border-none rounded-xl text-[#111111] lg:text-2xl'
value={employeeStatus} value={employeeStatus}
onChange={handleStatusChange} onChange={handleStatusChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
<button <button
type="submit" type='submit'
className="min-w-[100px] lg:min-w-[160px] m-2 p-2 border-none rounded-xl className='min-w-[100px] lg:min-w-[160px] m-2 p-2 border-none rounded-xl
text-center font-semibold lg:text-2xl hover:text-slate-300 text-center font-semibold lg:text-2xl hover:text-slate-300
hover:bg-gradient-to-bl hover:from-[#484848] hover:to-[#333333] hover:bg-gradient-to-bl hover:from-[#484848] hover:to-[#333333]
bg-gradient-to-br from-[#595959] to-[#444444]" bg-gradient-to-br from-[#595959] to-[#444444]'
onClick={update_status} onClick={update_status}
> >
Update Update

View File

@ -0,0 +1,48 @@
'use client';
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '~/lib/utils';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className='h-full w-full rounded-[inherit]'>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

58
src/components/ui/shadcn/button.tsx Normal file → Executable file
View File

@ -1,59 +1,59 @@
import * as React from "react" import * as React from 'react';
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm " + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm ' +
"font-medium ring-offset-background transition-colors focus-visible:outline-none " + 'font-medium ring-offset-background transition-colors focus-visible:outline-none ' +
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 " + 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ' +
"disabled:pointer-events-none disabled:opacity-50", 'disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} },
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button';
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Button.displayName = "Button" Button.displayName = 'Button';
export { Button, buttonVariants } export { Button, buttonVariants };

91
src/components/ui/shadcn/calendar.tsx Normal file → Executable file
View File

@ -1,11 +1,11 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { ChevronLeft, ChevronRight } from "lucide-react" import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from "react-day-picker" import { DayPicker } from 'react-day-picker';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { buttonVariants } from "~/components/ui/shadcn/button" import { buttonVariants } from '~/components/ui/shadcn/button';
export type CalendarProps = React.ComponentProps<typeof DayPicker> export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ function Calendar({
className, className,
@ -16,59 +16,60 @@ function Calendar({
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn("p-3", className)} className={cn('p-3', className)}
classNames={{ classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: "space-y-4", month: 'space-y-4',
caption: "flex justify-center pt-1 relative items-center", caption: 'flex justify-center pt-1 relative items-center',
caption_label: "text-sm font-medium", caption_label: 'text-sm font-medium',
nav: "space-x-1 flex items-center", nav: 'space-x-1 flex items-center',
nav_button: cn( nav_button: cn(
buttonVariants({ variant: "outline" }), buttonVariants({ variant: 'outline' }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
), ),
nav_button_previous: "absolute left-1", nav_button_previous: 'absolute left-1',
nav_button_next: "absolute right-1", nav_button_next: 'absolute right-1',
table: "w-full border-collapse space-y-1", table: 'w-full border-collapse space-y-1',
head_row: "flex", head_row: 'flex',
head_cell: head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: "flex w-full mt-2", row: 'flex w-full mt-2',
cell: "h-9 w-9 text-center text-sm p-0 relative " + cell:
"[&:has([aria-selected].day-range-end)]:rounded-r-md " + 'h-9 w-9 text-center text-sm p-0 relative ' +
"[&:has([aria-selected].day-outside)]:bg-accent/50 " + '[&:has([aria-selected].day-range-end)]:rounded-r-md ' +
"[&:has([aria-selected])]:bg-accent " + '[&:has([aria-selected].day-outside)]:bg-accent/50 ' +
"first:[&:has([aria-selected])]:rounded-l-md " + '[&:has([aria-selected])]:bg-accent ' +
"last:[&:has([aria-selected])]:rounded-r-md " + 'first:[&:has([aria-selected])]:rounded-l-md ' +
"focus-within:relative focus-within:z-20", 'last:[&:has([aria-selected])]:rounded-r-md ' +
'focus-within:relative focus-within:z-20',
day: cn( day: cn(
buttonVariants({ variant: "ghost" }), buttonVariants({ variant: 'ghost' }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100" 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
), ),
day_range_end: "day-range-end", day_range_end: 'day-range-end',
day_selected: day_selected:
"bg-primary text-primary-foreground hover:bg-primary " + 'bg-primary text-primary-foreground hover:bg-primary ' +
"hover:text-primary-foreground focus:bg-primary " + 'hover:text-primary-foreground focus:bg-primary ' +
"focus:text-primary-foreground", 'focus:text-primary-foreground',
day_today: "bg-accent text-accent-foreground", day_today: 'bg-accent text-accent-foreground',
day_outside: day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 " + 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 ' +
"aria-selected:text-muted-foreground aria-selected:opacity-30", 'aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: "text-muted-foreground opacity-50", day_disabled: 'text-muted-foreground opacity-50',
day_range_middle: day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground", 'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: "invisible", day_hidden: 'invisible',
...classNames, ...classNames,
}} }}
components={{ components={{
// @ts-expect-error - I didn't even write this code man cmon // @ts-expect-error - I didn't even write this code man cmon
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, IconLeft: ({ ...props }) => <ChevronLeft className='h-4 w-4' />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, IconRight: ({ ...props }) => <ChevronRight className='h-4 w-4' />,
}} }}
{...props} {...props}
/> />
) );
} }
Calendar.displayName = "Calendar" Calendar.displayName = 'Calendar';
export { Calendar } export { Calendar };

30
src/components/ui/shadcn/checkbox.tsx Normal file → Executable file
View File

@ -1,8 +1,8 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from "lucide-react" import { Check } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
@ -11,21 +11,21 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background " + 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background ' +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ' +
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 " + 'focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ' +
"data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", 'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className className,
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")} className={cn('flex items-center justify-center text-current')}
> >
<Check className="h-4 w-4" /> <Check className='h-4 w-4' />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

39
src/components/ui/shadcn/combobox.tsx Normal file → Executable file
View File

@ -1,8 +1,8 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Check, ChevronsUpDown } from "lucide-react" import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { Button } from "~/components/ui/shadcn/button" import { Button } from '~/components/ui/shadcn/button';
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@ -10,12 +10,12 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "~/components/ui/shadcn/command" } from '~/components/ui/shadcn/command';
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "~/components/ui/shadcn/popover" } from '~/components/ui/shadcn/popover';
// Define the type correctly as an array of objects // Define the type correctly as an array of objects
type ListItem = { type ListItem = {
@ -29,27 +29,28 @@ type ComboboxDemoProps = {
export function ComboboxDemo({ listItems }: ComboboxDemoProps) { export function ComboboxDemo({ listItems }: ComboboxDemoProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState('');
const selectedItem = listItems.find( const selectedItem =
(item) => item.value === value)?.label ?? "Select listItem..."; listItems.find((item) => item.value === value)?.label ??
'Select listItem...';
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant='outline'
role="combobox" role='combobox'
aria-expanded={open} aria-expanded={open}
className="w-[200px] justify-between" className='w-[200px] justify-between'
> >
{selectedItem} {selectedItem}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[200px] p-0"> <PopoverContent className='w-[200px] p-0'>
<Command> <Command>
<CommandInput placeholder="Search listItem..." /> <CommandInput placeholder='Search listItem...' />
<CommandEmpty>No Item found.</CommandEmpty> <CommandEmpty>No Item found.</CommandEmpty>
<CommandList> <CommandList>
<CommandGroup> <CommandGroup>
@ -58,14 +59,14 @@ export function ComboboxDemo({ listItems }: ComboboxDemoProps) {
key={listItem.value} key={listItem.value}
value={listItem.value} value={listItem.value}
onSelect={(currentValue) => { onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue); setValue(currentValue === value ? '' : currentValue);
setOpen(false); setOpen(false);
}} }}
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", 'mr-2 h-4 w-4',
value === listItem.value ? "opacity-100" : "opacity-0" value === listItem.value ? 'opacity-100' : 'opacity-0',
)} )}
/> />
{listItem.label} {listItem.label}

110
src/components/ui/shadcn/command.tsx Normal file → Executable file
View File

@ -1,10 +1,10 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { type DialogProps } from "@radix-ui/react-dialog" import { type DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from 'cmdk';
import { Search } from "lucide-react" import { Search } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { Dialog, DialogContent } from "~/components/ui/shadcn/dialog" import { Dialog, DialogContent } from '~/components/ui/shadcn/dialog';
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
@ -13,55 +13,57 @@ const Command = React.forwardRef<
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full flex-col overflow-hidden " + 'flex h-full w-full flex-col overflow-hidden ' +
"rounded-md bg-popover text-popover-foreground", 'rounded-md bg-popover text-popover-foreground',
className className,
)} )}
{...props} {...props}
/> />
)) ));
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {} interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className='overflow-hidden p-0 shadow-lg'>
<Command className="[&_[cmdk-group-heading]]:px-2 <Command
className='[&_[cmdk-group-heading]]:px-2
[&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:font-medium
[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:text-muted-foreground
[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2
[&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5
[&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3
[&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'
>
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} };
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm " + 'flex h-11 w-full rounded-md bg-transparent py-3 text-sm ' +
"outline-none placeholder:text-muted-foreground " + 'outline-none placeholder:text-muted-foreground ' +
"disabled:cursor-not-allowed disabled:opacity-50", 'disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
)) ));
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef< const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, React.ElementRef<typeof CommandPrimitive.List>,
@ -69,12 +71,12 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props} {...props}
/> />
)) ));
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, React.ElementRef<typeof CommandPrimitive.Empty>,
@ -82,12 +84,12 @@ const CommandEmpty = React.forwardRef<
>((props, ref) => ( >((props, ref) => (
<CommandPrimitive.Empty <CommandPrimitive.Empty
ref={ref} ref={ref}
className="py-6 text-center text-sm" className='py-6 text-center text-sm'
{...props} {...props}
/> />
)) ));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef< const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, React.ElementRef<typeof CommandPrimitive.Group>,
@ -96,17 +98,17 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 " + 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 ' +
"[&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs " + '[&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs ' +
"[&_[cmdk-group-heading]]:font-medium " + '[&_[cmdk-group-heading]]:font-medium ' +
"[&_[cmdk-group-heading]]:text-muted-foreground", '[&_[cmdk-group-heading]]:text-muted-foreground',
className className,
)} )}
{...props} {...props}
/> />
)) ));
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
@ -114,11 +116,11 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 h-px bg-border", className)} className={cn('-mx-1 h-px bg-border', className)}
{...props} {...props}
/> />
)) ));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef< const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, React.ElementRef<typeof CommandPrimitive.Item>,
@ -127,17 +129,17 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm " + 'relative flex cursor-default select-none items-center rounded-sm ' +
"px-2 py-1.5 text-sm outline-none aria-selected:bg-accent " + 'px-2 py-1.5 text-sm outline-none aria-selected:bg-accent ' +
"aria-selected:text-accent-foreground " + 'aria-selected:text-accent-foreground ' +
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className,
)} )}
{...props} {...props}
/> />
)) ));
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ const CommandShortcut = ({
className, className,
@ -146,14 +148,14 @@ const CommandShortcut = ({
return ( return (
<span <span
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", 'ml-auto text-xs tracking-widest text-muted-foreground',
className className,
)} )}
{...props} {...props}
/> />
) );
} };
CommandShortcut.displayName = "CommandShortcut" CommandShortcut.displayName = 'CommandShortcut';
export { export {
Command, Command,
@ -165,4 +167,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

34
src/components/ui/shadcn/date-picker.tsx Normal file → Executable file
View File

@ -1,41 +1,41 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { format } from "date-fns" import { format } from 'date-fns';
import { Calendar as CalendarIcon } from "lucide-react" import { Calendar as CalendarIcon } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { Button } from "~/components/ui/shadcn/button" import { Button } from '~/components/ui/shadcn/button';
import { Calendar } from "~/components/ui/shadcn/calendar" import { Calendar } from '~/components/ui/shadcn/calendar';
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "~/components/ui/shadcn/popover" } from '~/components/ui/shadcn/popover';
export function DatePickerDemo() { export function DatePickerDemo() {
const [date, setDate] = React.useState<Date>() const [date, setDate] = React.useState<Date>();
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={"outline"} variant={'outline'}
className={cn( className={cn(
"w-[280px] justify-start text-left font-normal", 'w-[280px] justify-start text-left font-normal',
!date && "text-muted-foreground" !date && 'text-muted-foreground',
)} )}
> >
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className='mr-2 h-4 w-4' />
{date ? format(date, "PPP") : <span>Pick a date</span>} {date ? format(date, 'PPP') : <span>Pick a date</span>}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0"> <PopoverContent className='w-auto p-0'>
<Calendar <Calendar
mode="single" mode='single'
selected={date} selected={date}
onSelect={setDate} onSelect={setDate}
//initialFocus // Causes an error idk if it's needed //initialFocus // Causes an error idk if it's needed
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) );
} }

96
src/components/ui/shadcn/dialog.tsx Normal file → Executable file
View File

@ -1,13 +1,13 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from "lucide-react" import { X } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -16,15 +16,15 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in " + 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in ' +
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 " + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 ' +
"data-[state=open]:fade-in-0", 'data-[state=open]:fade-in-0',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -35,31 +35,33 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] " + 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] ' +
"translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 " + 'translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 ' +
"data-[state=open]:animate-in data-[state=closed]:animate-out " + 'data-[state=open]:animate-in data-[state=closed]:animate-out ' +
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 " + 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 ' +
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 " + 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ' +
"data-[state=closed]:slide-out-to-left-1/2 " + 'data-[state=closed]:slide-out-to-left-1/2 ' +
"data-[state=closed]:slide-out-to-top-[48%] " + 'data-[state=closed]:slide-out-to-top-[48%] ' +
"data-[state=open]:slide-in-from-left-1/2 " + 'data-[state=open]:slide-in-from-left-1/2 ' +
"data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", 'data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 <DialogPrimitive.Close
className='absolute right-4 top-4 rounded-sm opacity-70
ring-offset-background transition-opacity hover:opacity-100 focus:outline-none ring-offset-background transition-opacity hover:opacity-100 focus:outline-none
focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none
data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> data-[state=open]:bg-accent data-[state=open]:text-muted-foreground'
<X className="h-4 w-4" /> >
<span className="sr-only">Close</span> <X className='h-4 w-4' />
<span className='sr-only'>Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -67,13 +69,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", 'flex flex-col space-y-1.5 text-center sm:text-left',
className className,
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -81,13 +83,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className className,
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -96,13 +98,13 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", 'text-lg font-semibold leading-none tracking-tight',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -110,11 +112,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@ -127,4 +129,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} };

62
src/components/ui/shadcn/drawer.tsx Normal file → Executable file
View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
@ -13,14 +13,14 @@ const Drawer = ({
shouldScaleBackground={shouldScaleBackground} shouldScaleBackground={shouldScaleBackground}
{...props} {...props}
/> />
) );
Drawer.displayName = "Drawer" Drawer.displayName = 'Drawer';
const DrawerTrigger = DrawerPrimitive.Trigger const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
@ -28,11 +28,11 @@ const DrawerOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)} className={cn('fixed inset-0 z-50 bg-black/80', className)}
{...props} {...props}
/> />
)) ));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
@ -43,39 +43,39 @@ const DrawerContent = React.forwardRef<
<DrawerPrimitive.Content <DrawerPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
className className,
)} )}
{...props} {...props}
> >
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> <div className='mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted' />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
)) ));
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = 'DrawerContent';
const DrawerHeader = ({ const DrawerHeader = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}
{...props} {...props}
/> />
) );
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = 'DrawerHeader';
const DrawerFooter = ({ const DrawerFooter = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props} {...props}
/> />
) );
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = 'DrawerFooter';
const DrawerTitle = React.forwardRef< const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
@ -84,13 +84,13 @@ const DrawerTitle = React.forwardRef<
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", 'text-lg font-semibold leading-none tracking-tight',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
@ -98,11 +98,11 @@ const DrawerDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export { export {
Drawer, Drawer,
@ -115,4 +115,4 @@ export {
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };

160
src/components/ui/shadcn/dropdown-menu.tsx Normal file → Executable file
View File

@ -1,38 +1,38 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm " + 'flex cursor-default select-none items-center rounded-sm ' +
"px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", 'px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && "pl-8", inset && 'pl-8',
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className='ml-auto h-4 w-4' />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -41,20 +41,20 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 " + 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 ' +
"text-popover-foreground shadow-lg data-[state=open]:animate-in " + 'text-popover-foreground shadow-lg data-[state=open]:animate-in ' +
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 " + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 ' +
"data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 " + 'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 ' +
"data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 " + 'data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 ' +
"data-[side=left]:slide-in-from-right-2 " + 'data-[side=left]:slide-in-from-right-2 ' +
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -65,41 +65,41 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 " + 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 ' +
"text-popover-foreground shadow-md data-[state=open]:animate-in " + 'text-popover-foreground shadow-md data-[state=open]:animate-in ' +
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 " + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 ' +
"data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 " + 'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 ' +
"data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 " + 'data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 ' +
"data-[side=left]:slide-in-from-right-2 " + 'data-[side=left]:slide-in-from-right-2 ' +
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm " + 'relative flex cursor-default select-none items-center rounded-sm ' +
"px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent " + 'px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent ' +
"focus:text-accent-foreground data-[disabled]:pointer-events-none " + 'focus:text-accent-foreground data-[disabled]:pointer-events-none ' +
"data-[disabled]:opacity-50", 'data-[disabled]:opacity-50',
inset && "pl-8", inset && 'pl-8',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@ -108,25 +108,25 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm " + 'relative flex cursor-default select-none items-center rounded-sm ' +
"py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent " + 'py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent ' +
"focus:text-accent-foreground data-[disabled]:pointer-events-none " + 'focus:text-accent-foreground data-[disabled]:pointer-events-none ' +
"data-[disabled]:opacity-50", 'data-[disabled]:opacity-50',
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className='h-4 w-4' />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -135,41 +135,41 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm " + 'relative flex cursor-default select-none items-center rounded-sm ' +
"py-1.5 pl-8 pr-2 text-sm outline-none transition-colors " + 'py-1.5 pl-8 pr-2 text-sm outline-none transition-colors ' +
"focus:bg-accent focus:text-accent-foreground " + 'focus:bg-accent focus:text-accent-foreground ' +
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" /> <Circle className='h-2 w-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", 'px-2 py-1.5 text-sm font-semibold',
inset && "pl-8", inset && 'pl-8',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@ -177,11 +177,11 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props} {...props}
/> />
)) ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({
className, className,
@ -189,12 +189,12 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props} {...props}
/> />
) );
} };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export { export {
DropdownMenu, DropdownMenu,
@ -212,4 +212,4 @@ export {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };

121
src/components/ui/shadcn/form.tsx Normal file → Executable file
View File

@ -1,36 +1,28 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import type * as LabelPrimitive from "@radix-ui/react-label" import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot';
import type { import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
ControllerProps, import { Controller, FormProvider, useFormContext } from 'react-hook-form';
FieldPath, import { cn } from '~/lib/utils';
FieldValues, import { Label } from '~/components/ui/shadcn/label';
} from "react-hook-form"
import {
Controller,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "~/lib/utils"
import { Label } from "~/components/ui/shadcn/label"
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue,
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
@ -38,21 +30,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error('useFormField should be used within <FormField>');
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -61,53 +53,54 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue,
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
ref={ref} ref={ref}
className={cn(error && "text-destructive", className)} className={cn(error && 'text-destructive', className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
@ -121,50 +114,50 @@ const FormControl = React.forwardRef<
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
ref={ref} ref={ref}
id={formDescriptionId} id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
<p <p
ref={ref} ref={ref}
id={formMessageId} id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)} className={cn('text-sm font-medium text-destructive', className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = 'FormMessage';
export { export {
useFormField, useFormField,
@ -175,4 +168,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };

26
src/components/ui/shadcn/input.tsx Normal file → Executable file
View File

@ -1,5 +1,5 @@
import * as React from "react" import * as React from 'react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -10,18 +10,18 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 " + 'flex h-10 w-full rounded-md border border-input bg-background px-3 ' +
"py-2 text-sm ring-offset-background file:border-0 file:bg-transparent " + 'py-2 text-sm ring-offset-background file:border-0 file:bg-transparent ' +
"file:text-sm file:font-medium placeholder:text-muted-foreground " + 'file:text-sm file:font-medium placeholder:text-muted-foreground ' +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ' +
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 'focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Input.displayName = "Input" Input.displayName = 'Input';
export { Input } export { Input };

22
src/components/ui/shadcn/label.tsx Normal file → Executable file
View File

@ -1,15 +1,15 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none " + 'text-sm font-medium leading-none ' +
"peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 'peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
) );
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
@ -21,7 +21,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)} className={cn(labelVariants(), className)}
{...props} {...props}
/> />
)) ));
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };

94
src/components/ui/shadcn/pagination.tsx Normal file → Executable file
View File

@ -1,111 +1,111 @@
import * as React from "react" import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { buttonVariants } from "~/components/ui/shadcn/button" import { buttonVariants } from '~/components/ui/shadcn/button';
import type { ButtonProps } from "~/components/ui/shadcn/button" import type { ButtonProps } from '~/components/ui/shadcn/button';
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav <nav
role="navigation" role='navigation'
aria-label="pagination" aria-label='pagination'
className={cn("mx-auto flex w-full justify-center", className)} className={cn('mx-auto flex w-full justify-center', className)}
{...props} {...props}
/> />
) );
Pagination.displayName = "Pagination" Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef< const PaginationContent = React.forwardRef<
HTMLUListElement, HTMLUListElement,
React.ComponentProps<"ul"> React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ul <ul
ref={ref} ref={ref}
className={cn("flex flex-row items-center gap-1", className)} className={cn('flex flex-row items-center gap-1', className)}
{...props} {...props}
/> />
)) ));
PaginationContent.displayName = "PaginationContent" PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef< const PaginationItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
React.ComponentProps<"li"> React.ComponentProps<'li'>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} /> <li ref={ref} className={cn('', className)} {...props} />
)) ));
PaginationItem.displayName = "PaginationItem" PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean;
} & Pick<ButtonProps, "size"> & } & Pick<ButtonProps, 'size'> &
React.ComponentProps<"a"> React.ComponentProps<'a'>;
const PaginationLink = ({ const PaginationLink = ({
className, className,
isActive, isActive,
size = "icon", size = 'icon',
...props ...props
}: PaginationLinkProps) => ( }: PaginationLinkProps) => (
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? 'page' : undefined}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: isActive ? "outline" : "ghost", variant: isActive ? 'outline' : 'ghost',
size, size,
}), }),
className className,
)} )}
{...props} {...props}
/> />
) );
PaginationLink.displayName = "PaginationLink" PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({ const PaginationPrevious = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label='Go to previous page'
size="default" size='default'
className={cn("gap-1 pl-2.5", className)} className={cn('gap-1 pl-2.5', className)}
{...props} {...props}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className='h-4 w-4' />
<span>Previous</span> <span>Previous</span>
</PaginationLink> </PaginationLink>
) );
PaginationPrevious.displayName = "PaginationPrevious" PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({ const PaginationNext = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label='Go to next page'
size="default" size='default'
className={cn("gap-1 pr-2.5", className)} className={cn('gap-1 pr-2.5', className)}
{...props} {...props}
> >
<span>Next</span> <span>Next</span>
<ChevronRight className="h-4 w-4" /> <ChevronRight className='h-4 w-4' />
</PaginationLink> </PaginationLink>
) );
PaginationNext.displayName = "PaginationNext" PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({ const PaginationEllipsis = ({
className, className,
...props ...props
}: React.ComponentProps<"span">) => ( }: React.ComponentProps<'span'>) => (
<span <span
aria-hidden aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)} className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props} {...props}
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className='h-4 w-4' />
<span className="sr-only">More pages</span> <span className='sr-only'>More pages</span>
</span> </span>
) );
PaginationEllipsis.displayName = "PaginationEllipsis" PaginationEllipsis.displayName = 'PaginationEllipsis';
export { export {
Pagination, Pagination,
@ -115,4 +115,4 @@ export {
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} };

34
src/components/ui/shadcn/popover.tsx Normal file → Executable file
View File

@ -1,36 +1,36 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md " + 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md ' +
"outline-none data-[state=open]:animate-in data-[state=closed]:animate-out " + 'outline-none data-[state=open]:animate-in data-[state=closed]:animate-out ' +
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 " + 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 ' +
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 " + 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ' +
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 " + 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 ' +
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverTrigger, PopoverContent };

20
src/components/ui/shadcn/progress.tsx Normal file → Executable file
View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
@ -12,17 +12,17 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary", 'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
className className,
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all" className='h-full w-full flex-1 bg-primary transition-all'
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }} style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
)) ));
Progress.displayName = ProgressPrimitive.Root.displayName Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress } export { Progress };

40
src/components/ui/shadcn/radio-group.tsx Normal file → Executable file
View File

@ -1,8 +1,8 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Circle } from "lucide-react" import { Circle } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const RadioGroup = React.forwardRef< const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>, React.ElementRef<typeof RadioGroupPrimitive.Root>,
@ -10,13 +10,13 @@ const RadioGroup = React.forwardRef<
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<RadioGroupPrimitive.Root <RadioGroupPrimitive.Root
className={cn("grid gap-2", className)} className={cn('grid gap-2', className)}
{...props} {...props}
ref={ref} ref={ref}
/> />
) );
}) });
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef< const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>, React.ElementRef<typeof RadioGroupPrimitive.Item>,
@ -26,20 +26,20 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary " + 'aspect-square h-4 w-4 rounded-full border border-primary text-primary ' +
"ring-offset-background focus:outline-none focus-visible:ring-2 " + 'ring-offset-background focus:outline-none focus-visible:ring-2 ' +
"focus-visible:ring-ring focus-visible:ring-offset-2 " + 'focus-visible:ring-ring focus-visible:ring-offset-2 ' +
"disabled:cursor-not-allowed disabled:opacity-50", 'disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
{...props} {...props}
> >
<RadioGroupPrimitive.Indicator className="flex items-center justify-center"> <RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
<Circle className="h-2.5 w-2.5 fill-current text-current" /> <Circle className='h-2.5 w-2.5 fill-current text-current' />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
) );
}) });
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem } export { RadioGroup, RadioGroupItem };

View File

@ -0,0 +1,48 @@
'use client';
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '~/lib/utils';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className='h-full w-full rounded-[inherit]'>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

122
src/components/ui/shadcn/select.tsx Normal file → Executable file
View File

@ -1,12 +1,12 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from "lucide-react" import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@ -15,22 +15,22 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border " + 'flex h-10 w-full items-center justify-between rounded-md border ' +
"border-input bg-background px-3 py-2 text-sm ring-offset-background " + 'border-input bg-background px-3 py-2 text-sm ring-offset-background ' +
"placeholder:text-muted-foreground focus:outline-none focus:ring-2 " + 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 ' +
"focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed " + 'focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed ' +
"disabled:opacity-50 [&>span]:line-clamp-1", 'disabled:opacity-50 [&>span]:line-clamp-1',
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className='h-4 w-4 opacity-50' />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@ -39,15 +39,15 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", 'flex cursor-default items-center justify-center py-1',
className className,
)} )}
{...props} {...props}
> >
<ChevronUp className="h-4 w-4" /> <ChevronUp className='h-4 w-4' />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
)) ));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@ -56,36 +56,36 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", 'flex cursor-default items-center justify-center py-1',
className className,
)} )}
{...props} {...props}
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className='h-4 w-4' />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ));
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border " + 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border ' +
"bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in " + 'bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in ' +
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 " + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 ' +
"data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 " + 'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 ' +
"data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 " + 'data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 ' +
"data-[side=left]:slide-in-from-right-2 " + 'data-[side=left]:slide-in-from-right-2 ' +
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === "popper" && position === 'popper' &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 " + 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 ' +
"data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 'data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className className,
)} )}
position={position} position={position}
{...props} {...props}
@ -93,10 +93,10 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", 'p-1',
position === "popper" && position === 'popper' &&
"h-[var(--radix-select-trigger-height)] w-full " + 'h-[var(--radix-select-trigger-height)] w-full ' +
"min-w-[var(--radix-select-trigger-width)]" 'min-w-[var(--radix-select-trigger-width)]',
)} )}
> >
{children} {children}
@ -104,8 +104,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ));
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
@ -113,11 +113,11 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
ref={ref} ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props} {...props}
/> />
)) ));
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
@ -126,24 +126,24 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm " + 'relative flex w-full cursor-default select-none items-center rounded-sm ' +
"py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent " + 'py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent ' +
"focus:text-accent-foreground data-[disabled]:pointer-events-none " + 'focus:text-accent-foreground data-[disabled]:pointer-events-none ' +
"data-[disabled]:opacity-50", 'data-[disabled]:opacity-50',
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className='h-4 w-4' />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ));
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
@ -151,11 +151,11 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props} {...props}
/> />
)) ));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
@ -168,4 +168,4 @@ export {
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} };

34
src/components/ui/shadcn/switch.tsx Normal file → Executable file
View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@ -11,26 +11,26 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full " + 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full ' +
"border-2 border-transparent transition-colors focus-visible:outline-none " + 'border-2 border-transparent transition-colors focus-visible:outline-none ' +
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 " + 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ' +
"focus-visible:ring-offset-background disabled:cursor-not-allowed " + 'focus-visible:ring-offset-background disabled:cursor-not-allowed ' +
"disabled:opacity-50 data-[state=checked]:bg-primary " + 'disabled:opacity-50 data-[state=checked]:bg-primary ' +
"data-[state=unchecked]:bg-input", 'data-[state=unchecked]:bg-input',
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg " + 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ' +
"ring-0 transition-transform data-[state=checked]:translate-x-5 " + 'ring-0 transition-transform data-[state=checked]:translate-x-5 ' +
"data-[state=unchecked]:translate-x-0" 'data-[state=unchecked]:translate-x-0',
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
)) ));
Switch.displayName = SwitchPrimitives.Root.displayName Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch } export { Switch };

64
src/components/ui/shadcn/table.tsx Normal file → Executable file
View File

@ -1,28 +1,28 @@
import * as React from "react" import * as React from 'react';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const Table = React.forwardRef< const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className='relative w-full overflow-auto'>
<table <table
ref={ref} ref={ref}
className={cn("w-full caption-bottom text-sm", className)} className={cn('w-full caption-bottom text-sm', className)}
{...props} {...props}
/> />
</div> </div>
)) ));
Table.displayName = "Table" Table.displayName = 'Table';
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
)) ));
TableHeader.displayName = "TableHeader" TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef< const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@ -30,11 +30,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody
ref={ref} ref={ref}
className={cn("[&_tr:last-child]:border-0", className)} className={cn('[&_tr:last-child]:border-0', className)}
{...props} {...props}
/> />
)) ));
TableBody.displayName = "TableBody" TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@ -43,13 +43,13 @@ const TableFooter = React.forwardRef<
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableFooter.displayName = "TableFooter" TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef< const TableRow = React.forwardRef<
HTMLTableRowElement, HTMLTableRowElement,
@ -58,13 +58,13 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableRow.displayName = "TableRow" TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -73,14 +73,14 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium " + 'h-12 px-4 text-left align-middle font-medium ' +
"text-muted-foreground [&:has([role=checkbox])]:pr-0", 'text-muted-foreground [&:has([role=checkbox])]:pr-0',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableHead.displayName = "TableHead" TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef< const TableCell = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -88,11 +88,11 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props} {...props}
/> />
)) ));
TableCell.displayName = "TableCell" TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
@ -100,11 +100,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)} className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
TableCaption.displayName = "TableCaption" TableCaption.displayName = 'TableCaption';
export { export {
Table, Table,
@ -115,4 +115,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

32
src/components/ui/shadcn/toaster.tsx Normal file → Executable file
View File

@ -1,31 +1,31 @@
"use client" 'use client';
import { useTheme } from "next-themes" import { useTheme } from 'next-themes';
import { Toaster as Sonner } from "sonner" import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Sonner> type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = 'system' } = useTheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps['theme']}
className="toaster group" className='toaster group'
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: toast:
"group toast group-[.toaster]:bg-background " + 'group toast group-[.toaster]:bg-background ' +
"group-[.toaster]:text-foreground " + 'group-[.toaster]:text-foreground ' +
"group-[.toaster]:border-border group-[.toaster]:shadow-lg", 'group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: "group-[.toast]:text-muted-foreground", description: 'group-[.toast]:text-muted-foreground',
actionButton: actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
}, },
}} }}
{...props} {...props}
/> />
) );
} };
export { Toaster } export { Toaster };

36
src/components/ui/shadcn/toggle-group.tsx Normal file → Executable file
View File

@ -1,16 +1,16 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from "class-variance-authority" import { type VariantProps } from 'class-variance-authority';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
import { toggleVariants } from "~/components/ui/shadcn/toggle" import { toggleVariants } from '~/components/ui/shadcn/toggle';
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>({ >({
size: "default", size: 'default',
variant: "default", variant: 'default',
}) });
const ToggleGroup = React.forwardRef< const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>, React.ElementRef<typeof ToggleGroupPrimitive.Root>,
@ -19,23 +19,23 @@ const ToggleGroup = React.forwardRef<
>(({ className, variant, size, children, ...props }, ref) => ( >(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
ref={ref} ref={ref}
className={cn("flex items-center justify-center gap-1", className)} className={cn('flex items-center justify-center gap-1', className)}
{...props} {...props}
> >
<ToggleGroupContext.Provider value={{ variant, size }}> <ToggleGroupContext.Provider value={{ variant, size }}>
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
)) ));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef< const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>, React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => { >(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
@ -45,15 +45,15 @@ const ToggleGroupItem = React.forwardRef<
variant: context.variant ?? variant, variant: context.variant ?? variant,
size: context.size ?? size, size: context.size ?? size,
}), }),
className className,
)} )}
{...props} {...props}
> >
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
) );
}) });
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem };

46
src/components/ui/shadcn/toggle.tsx Normal file → Executable file
View File

@ -1,35 +1,35 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as TogglePrimitive from "@radix-ui/react-toggle" import * as TogglePrimitive from '@radix-ui/react-toggle';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium " + 'inline-flex items-center justify-center rounded-md text-sm font-medium ' +
"ring-offset-background transition-colors hover:bg-muted " + 'ring-offset-background transition-colors hover:bg-muted ' +
"hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 " + 'hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 ' +
"focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none " + 'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none ' +
"disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 'disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: 'bg-transparent',
outline: outline:
"border border-input bg-transparent hover:bg-accent " + 'border border-input bg-transparent hover:bg-accent ' +
"hover:text-accent-foreground", 'hover:text-accent-foreground',
}, },
size: { size: {
default: "h-10 px-3", default: 'h-10 px-3',
sm: "h-9 px-2.5", sm: 'h-9 px-2.5',
lg: "h-11 px-5", lg: 'h-11 px-5',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} },
) );
const Toggle = React.forwardRef< const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>, React.ElementRef<typeof TogglePrimitive.Root>,
@ -41,8 +41,8 @@ const Toggle = React.forwardRef<
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
/> />
)) ));
Toggle.displayName = TogglePrimitive.Root.displayName Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants } export { Toggle, toggleVariants };

34
src/components/ui/shadcn/tooltip.tsx Normal file → Executable file
View File

@ -1,11 +1,11 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from "~/lib/utils" import { cn } from '~/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@ -15,16 +15,16 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 " + 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 ' +
"text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 " + 'text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 ' +
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 " + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 ' +
"data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 " + 'data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 ' +
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 " + 'data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 ' +
"data-[side=top]:slide-in-from-bottom-2", 'data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

21
src/env.js Normal file → Executable file
View File

@ -1,5 +1,5 @@
import { createEnv } from "@t3-oss/env-nextjs"; import { createEnv } from '@t3-oss/env-nextjs';
import { z } from "zod"; import { z } from 'zod';
export const env = createEnv({ export const env = createEnv({
/** /**
@ -9,14 +9,18 @@ export const env = createEnv({
server: { server: {
DATABASE_URL: z.string().url(), DATABASE_URL: z.string().url(),
NODE_ENV: z NODE_ENV: z
.enum(["development", "test", "production"]) .enum(['development', 'test', 'production'])
.default("development"), .default('development'),
API_KEY: z.string(), API_KEY: z.string(),
AUTH_TRUST_HOST: z.boolean().default(false), AUTH_URL: z.string(),
AUTH_TRUST_HOST: z.coerce.boolean().default(true),
AUTH_SECRET: z.string(), AUTH_SECRET: z.string(),
AUTH_MICROSOFT_ENTRA_ID_ID: z.string(), AUTH_MICROSOFT_ENTRA_ID_ID: z.string(),
AUTH_MICROSOFT_ENTRA_ID_SECRET: z.string(), AUTH_MICROSOFT_ENTRA_ID_SECRET: z.string(),
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: z.string(), AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: z.string(),
AUTH_AUTHENTIK_CLIENT_ID: z.string(),
AUTH_AUTHENTIK_CLIENT_SECRET: z.string(),
AUTH_AUTHENTIK_ISSUER: z.string(),
}, },
/** /**
@ -36,11 +40,16 @@ export const env = createEnv({
DATABASE_URL: process.env.DATABASE_URL, DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
API_KEY: process.env.API_KEY, API_KEY: process.env.API_KEY,
AUTH_URL: process.env.AUTH_URL,
AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST, AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST,
AUTH_SECRET: process.env.AUTH_SECRET, AUTH_SECRET: process.env.AUTH_SECRET,
AUTH_MICROSOFT_ENTRA_ID_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, AUTH_MICROSOFT_ENTRA_ID_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
AUTH_MICROSOFT_ENTRA_ID_SECRET: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, AUTH_MICROSOFT_ENTRA_ID_SECRET: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID, AUTH_MICROSOFT_ENTRA_ID_TENANT_ID:
process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID,
AUTH_AUTHENTIK_CLIENT_ID: process.env.AUTH_AUTHENTIK_CLIENT_ID,
AUTH_AUTHENTIK_CLIENT_SECRET: process.env.AUTH_AUTHENTIK_CLIENT_SECRET,
AUTH_AUTHENTIK_ISSUER: process.env.AUTH_AUTHENTIK_ISSUER,
}, },
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially

6
src/lib/utils.ts Normal file → Executable file
View File

@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

2
src/middleware.ts Normal file → Executable file
View File

@ -1 +1 @@
export { auth as middleware } from "~/auth" export { auth as middleware } from '~/auth';

12
src/server/db/index.ts Normal file → Executable file
View File

@ -1,8 +1,8 @@
import { drizzle } from "drizzle-orm/mysql2"; import { drizzle } from 'drizzle-orm/mysql2';
import { createPool, type Pool } from "mysql2/promise"; import { createPool, type Pool } from 'mysql2/promise';
import { env } from "~/env"; import { env } from '~/env';
import * as schema from "./schema"; import * as schema from './schema';
/** /**
* Cache the database connection in development. This avoids creating a new connection on every HMR * Cache the database connection in development. This avoids creating a new connection on every HMR
@ -13,6 +13,6 @@ const globalForDb = globalThis as unknown as {
}; };
const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL }); const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL });
if (env.NODE_ENV !== "production") globalForDb.conn = conn; if (env.NODE_ENV !== 'production') globalForDb.conn = conn;
export const db = drizzle(conn, { schema, mode: "default" }); export const db = drizzle(conn, { schema, mode: 'default' });

38
src/server/db/schema.ts Normal file → Executable file
View File

@ -1,32 +1,26 @@
// https://orm.drizzle.team/docs/sql-schema-declaration // https://orm.drizzle.team/docs/sql-schema-declaration
import { sql } from "drizzle-orm"; import { sql } from 'drizzle-orm';
import { import {
bigint, bigint,
mysqlTableCreator, mysqlTableCreator,
timestamp, timestamp,
varchar, varchar,
} from "drizzle-orm/mysql-core"; } from 'drizzle-orm/mysql-core';
export const createTable = mysqlTableCreator((name) => `${name}`); export const createTable = mysqlTableCreator((name) => `${name}`);
export const users = createTable( export const users = createTable('users', {
"users", id: bigint('id', { mode: 'number' }).primaryKey().autoincrement(),
{ name: varchar('name', { length: 256 }).notNull(),
id: bigint("id", {mode: "number"}).primaryKey().autoincrement(), status: varchar('status', { length: 256 }).notNull(),
name: varchar("name", { length: 256 }).notNull(), updatedAt: timestamp('updatedAt')
status: varchar("status", { length: 256 }).notNull(), .default(sql`CURRENT_TIMESTAMP`)
updatedAt: timestamp("updatedAt") .notNull(),
.default(sql`CURRENT_TIMESTAMP`) });
.notNull(),
},
);
export const history = createTable( export const history = createTable('history', {
"history", id: bigint('id', { mode: 'number' }).primaryKey().autoincrement(),
{ user_id: bigint('user_id', { mode: 'number' }).references(() => users.id),
id: bigint("id", {mode: "number"}).primaryKey().autoincrement(), status: varchar('status', { length: 256 }).notNull(),
user_id: bigint("user_id", {mode: "number"}).references(() => users.id), updatedAt: timestamp('updatedAt').notNull(),
status: varchar("status", { length: 256 }).notNull(), });
updatedAt: timestamp("updatedAt").notNull(),
},
);

97
src/server/functions.ts Normal file → Executable file
View File

@ -1,6 +1,6 @@
import "server-only"; import 'server-only';
import { db } from "~/server/db"; import { db } from '~/server/db';
import { sql } from "drizzle-orm"; import { sql } from 'drizzle-orm';
export const getEmployees = async () => { export const getEmployees = async () => {
return await db.query.users.findMany({ return await db.query.users.findMany({
@ -10,15 +10,17 @@ export const getEmployees = async () => {
// Update Employee Status uses Raw SQL because Drizzle ORM doesn't support // Update Employee Status uses Raw SQL because Drizzle ORM doesn't support
// update with MySQL // update with MySQL
export const updateEmployeeStatus = export const updateEmployeeStatus = async (
async (employeeIds: string[], newStatus: string) => { employeeIds: string[],
newStatus: string,
) => {
try { try {
// Convert array of ids to a format suitable for SQL query (comma-separated string) // Convert array of ids to a format suitable for SQL query (comma-separated string)
const idList = employeeIds.map(id => parseInt(id, 10)); const idList = employeeIds.map((id) => parseInt(id, 10));
let updatedAt = new Date(); let updatedAt = new Date();
// Not sure why but localhost is off by 5 hours // Not sure why but localhost is off by 5 hours
if (process.env.NODE_ENV === 'development') if (process.env.NODE_ENV === 'development')
updatedAt = new Date(updatedAt.setHours(updatedAt.getUTCHours())+ 5); updatedAt = new Date(updatedAt.setHours(updatedAt.getUTCHours()) + 5);
const query = sql` const query = sql`
UPDATE users UPDATE users
SET status = ${newStatus}, updatedAt = ${updatedAt} SET status = ${newStatus}, updatedAt = ${updatedAt}
@ -27,14 +29,15 @@ export const updateEmployeeStatus =
await db.execute(query); await db.execute(query);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error("Error updating employee status:", error); console.error('Error updating employee status:', error);
throw new Error("Failed to update status"); throw new Error('Failed to update status');
} }
}; };
// Function to Update Employee Status by Name using Raw SQL // Function to Update Employee Status by Name using Raw SQL
export const updateEmployeeStatusByName = export const updateEmployeeStatusByName = async (
async (technicians:{ name: string, status: string }[]) => { technicians: { name: string; status: string }[],
) => {
try { try {
for (const technician of technicians) { for (const technician of technicians) {
const { name, status } = technician; const { name, status } = technician;
@ -47,57 +50,86 @@ export const updateEmployeeStatusByName =
} }
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error("Error updating employee status by name:", error); console.error('Error updating employee status by name:', error);
throw new Error("Failed to update status by name"); throw new Error('Failed to update status by name');
} }
}; };
// Type definitions for Paginated History API // Type definitions for Paginated History API
interface HistoryEntry { type HistoryEntry = {
name: string; name: string;
status: string; status: string;
time: Date; updatedAt: Date;
} };
interface PaginatedHistory { type PaginatedHistory = {
data: HistoryEntry[]; data: HistoryEntry[];
meta: { meta: {
current_page: number; current_page: number;
per_page: number; per_page: number;
total_pages: number; total_pages: number;
total_count: number; total_count: number;
} };
} };
export const getHistory = export const get_history = async (
async (page: number, perPage: number): Promise<PaginatedHistory> => { user_id: number,
page: number,
perPage: number,
): Promise<PaginatedHistory> => {
const offset = (page - 1) * perPage; const offset = (page - 1) * perPage;
const historyQuery = sql` let historyQuery = sql`
SELECT u.name, h.status, h.updatedAt SELECT u.name, h.status, h.updatedAt
FROM history h FROM history h
JOIN users u ON h.user_id = u.id JOIN users u ON h.user_id = u.id
WHERE h.user_id = ${user_id}
ORDER BY h.id DESC ORDER BY h.id DESC
LIMIT ${perPage} OFFSET ${offset} LIMIT ${perPage} OFFSET ${offset}
`; `;
const countQuery = sql` let countQuery = sql`
SELECT COUNT(*) AS total_count SELECT COUNT(*) AS total_count
FROM history FROM history
WHERE user_id = ${user_id}
`; `;
const [historyResults, countResults] = await Promise.all([ if (user_id === -1) {
db.execute(historyQuery), historyQuery = sql`
SELECT u.name, h.status, h.updatedAt
FROM history h
JOIN users u ON h.user_id = u.id
ORDER BY h.id DESC
LIMIT ${perPage} OFFSET ${offset}
`;
countQuery = sql`
SELECT COUNT(*) AS total_count
FROM history
`;
}
const [historyresults, countresults] = await Promise.all([
db.execute(historyQuery),
db.execute(countQuery), db.execute(countQuery),
]); ]);
// Safely cast results // Safely cast results
const historyRows = historyResults[0] as unknown as const historyrows = historyresults[0] as unknown as {
{ name: string, status: string, updatedAt: Date }[]; name: string;
const countRow = countResults[0] as unknown as { total_count: number }[]; status: string;
const totalCount = countRow[0]?.total_count ?? 0; updatedAt: Date;
}[];
const countrow = countresults[0] as unknown as { total_count: number }[];
const totalCount = countrow[0]?.total_count ?? 0;
const totalPages = Math.ceil(totalCount / perPage); const totalPages = Math.ceil(totalCount / perPage);
// Format and map results // Format and map results
const formattedResults: HistoryEntry[] = historyRows.map(row => ({ let formattedResults: HistoryEntry[] = historyrows.map((row) => ({
name: row.name, name: row.name,
status: row.status, status: row.status,
time: new Date(row.updatedAt), updatedAt: new Date(row.updatedAt),
})); }));
if (process.env.NODE_ENV === 'development') {
formattedResults = formattedResults.map((entry) => ({
...entry,
updatedAt: new Date(
entry.updatedAt.setHours(entry.updatedAt.getUTCHours() + 14),
),
}));
}
return { return {
data: formattedResults, data: formattedResults,
meta: { meta: {
@ -105,7 +137,6 @@ export const getHistory =
per_page: perPage, per_page: perPage,
total_pages: totalPages, total_pages: totalPages,
total_count: totalCount, total_count: totalCount,
} },
}; };
}; };

5
src/styles/globals.css Normal file → Executable file
View File

@ -128,9 +128,12 @@
.techtable-fullscreen { .techtable-fullscreen {
width: 100%; width: 100%;
height: 100%; /*height: 100%;*/
height: 80vh;
} }
.tablefill { .tablefill {
height: 10vh; height: 10vh;
} }

82
tailwind.config.ts Normal file → Executable file
View File

@ -1,84 +1,84 @@
import type { Config } from "tailwindcss" import type { Config } from 'tailwindcss';
import { fontFamily } from "tailwindcss/defaultTheme" import { fontFamily } from 'tailwindcss/defaultTheme';
const config = { const config = {
darkMode: ["class"], darkMode: ['class'],
content: [ content: [
'./pages/**/*.{ts,tsx}', './pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}', './components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}', './app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}', './src/**/*.{ts,tsx}',
], ],
prefix: "", prefix: '',
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px',
}, },
}, },
extend: { extend: {
fontFamily: { fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans], sans: ['var(--font-sans)', ...fontFamily.sans],
}, },
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))',
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))',
}, },
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)',
}, },
keyframes: { keyframes: {
"accordion-down": { 'accordion-down': {
from: { height: "0" }, from: { height: '0' },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: 'var(--radix-accordion-content-height)' },
}, },
"accordion-up": { 'accordion-up': {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: 'var(--radix-accordion-content-height)' },
to: { height: "0" }, to: { height: '0' },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", 'accordion-down': 'accordion-down 0.2s ease-out',
"accordion-up": "accordion-up 0.2s ease-out", 'accordion-up': 'accordion-up 0.2s ease-out',
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require('tailwindcss-animate')],
} satisfies Config } satisfies Config;
export default config export default config;

0
tsconfig.json Normal file → Executable file
View File