Compare commits
104
Commits
d68806b2f2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c7b09f09 | ||
|
|
dbf6cfbc12 | ||
|
|
02b4437eb3 | ||
|
|
b1238d4363 | ||
|
|
94a0328cad | ||
|
|
72a49fc2d0 | ||
|
|
e6cbdcc51d | ||
|
|
9a9880d865 | ||
|
|
960eaabe42 | ||
|
|
43dad88aea | ||
|
|
5fe2746879 | ||
|
|
e288be64da | ||
|
|
b4a72b7154 | ||
|
|
32b561a96b | ||
|
|
e75444ba52 | ||
|
|
6270c2701d | ||
|
|
db07b5ae0c | ||
|
|
6f4a021785 | ||
|
|
142a814039 | ||
|
|
5df8306909 | ||
|
|
b0098c9270 | ||
|
|
29cec47928 | ||
|
|
47da9bb340 | ||
|
|
e5d3a319a8 | ||
|
|
f5b4f6e2d0 | ||
|
|
57f77a3373 | ||
|
|
dae0baea3b | ||
|
|
3ce2b9662c | ||
|
|
c5a5611e10 | ||
|
|
c4ff57876a | ||
|
|
2e82223689 | ||
|
|
738f533d4c | ||
|
|
b1045c1dd2 | ||
|
|
7df7575a05 | ||
|
|
0b4a05e3d6 | ||
|
|
c09f187465 | ||
|
|
f5a05ef4a1 | ||
|
|
0810ff1373 | ||
|
|
fb91f3df90 | ||
|
|
c9abfc4815 | ||
|
|
40b6fd3cfc | ||
|
|
8a83096ce6 | ||
|
|
be8201b60a | ||
|
|
e87d41e08a | ||
|
|
57a0891224 | ||
|
|
0bd9e49964 | ||
|
|
186c8b172b | ||
|
|
456fc31770 | ||
|
|
145c79c8b2 | ||
|
|
0b634a0a2a | ||
|
|
a2f9bc0c35 | ||
|
|
9abbe01a7b | ||
|
|
72c4fce25e | ||
|
|
d52604971d | ||
|
|
66c8b90d79 | ||
|
|
978db63a95 | ||
|
|
dcc6fc2c0a | ||
|
|
29b9a6618c | ||
|
|
caf9f4dd5e | ||
|
|
a6a9d1e236 | ||
|
|
9a265a760d | ||
|
|
57cdb609b8 | ||
|
|
5b51b2f6e9 | ||
|
|
bd4581ea2b | ||
|
|
5f9dd1a5c4 | ||
|
|
f7e621ef66 | ||
|
|
2e19b72ce2 | ||
|
|
3f1b11496e | ||
|
|
8a46d734a9 | ||
|
|
943126f6b3 | ||
|
|
d13b984ac4 | ||
|
|
78ed092e1c | ||
|
|
692d54bab1 | ||
|
|
bf75d9deef | ||
|
|
bc64b74a34 | ||
|
|
e057dd50c8 | ||
|
|
d471418c8f | ||
|
|
2bf524ff82 | ||
|
|
2afe84e608 | ||
|
|
c17aaeb9f1 | ||
|
|
cf3c6324c8 | ||
|
|
587c7137c5 | ||
|
|
79b53addf4 | ||
|
|
74ebd6bc10 | ||
|
|
d04b3d2b22 | ||
|
|
2c5b865c66 | ||
|
|
ea02e5d358 | ||
|
|
7138e4a222 | ||
|
|
a70e08ac33 | ||
|
|
a93cecd0ee | ||
|
|
31ba33ee4d | ||
|
|
5b878d9688 | ||
|
|
99718c8698 | ||
|
|
31821f84cb | ||
|
|
afeca88f80 | ||
|
|
9ee81d06a2 | ||
|
|
582d1901f6 | ||
|
|
c5b96779aa | ||
|
|
d54aa7e2c2 | ||
|
|
6c6330cd7a | ||
|
|
e288f659c2 | ||
|
|
faebb51d90 | ||
|
|
8093dd2b7f | ||
|
|
046f201e63 |
No files matched your search
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
build
|
||||
.git
|
||||
.env*
|
||||
!.env.example
|
||||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
.planning
|
||||
*.log
|
||||
.husky
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,14 @@
|
||||
# Better Auth
|
||||
# Generate with: bunx @better-auth/cli secret
|
||||
BETTER_AUTH_SECRET=
|
||||
# Full absolute URL of the deployed app, e.g. https://example.com
|
||||
BETTER_AUTH_URL=
|
||||
|
||||
# Public auth URL for the browser client (must match BETTER_AUTH_URL in prod)
|
||||
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Optional: database URL for better-auth (Kysely/Drizzle adapter)
|
||||
# DATABASE_URL=postgres://user:pass@localhost:5432/db
|
||||
|
||||
# Optional: OpenAPI document URL used by openapi-fetch (generate client from it)
|
||||
# OPENAPI_URL=/api/openapi.json
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Verify CI check-runs before enabling auto-merge
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const sha = context.payload.pull_request.head.sha;
|
||||
|
||||
// Wait for in-progress runs (with timeout), then collect conclusions
|
||||
const requiredContext = "lint + typecheck + test + build";
|
||||
let conclusion = null;
|
||||
for (let attempt = 0; attempt < 60; attempt++) {
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner, repo, ref: sha,
|
||||
});
|
||||
const match = checks.check_runs.find(
|
||||
(r) => r.name === requiredContext || r.output?.title === requiredContext
|
||||
);
|
||||
if (match && match.status === "completed") {
|
||||
conclusion = match.conclusion;
|
||||
break;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 10000));
|
||||
}
|
||||
if (!conclusion) {
|
||||
core.setFailed(`Required check "${requiredContext}" never completed within timeout.`);
|
||||
return;
|
||||
}
|
||||
if (conclusion !== "success") {
|
||||
core.setFailed(`Required check "${requiredContext}" concluded ${conclusion}. NOT merging.`);
|
||||
return;
|
||||
}
|
||||
core.info(`Check "${requiredContext}" passed — safe to enable auto-merge.`);
|
||||
|
||||
- name: Enable auto-merge for Dependabot PR
|
||||
if: success()
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,57 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
groups:
|
||||
production:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
development:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
ignore:
|
||||
# ---- Toolchain pins: semver-major bumps break the lint/typecheck stack ----
|
||||
# ESLint 10 dropped rule-context getFilename(); eslint-plugin-react (^7.37.x)
|
||||
# peer range caps at eslint ^9.7 -> react/display-name crash at load.
|
||||
- dependency-name: "eslint"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# typescript-eslint 8.x peer range is ">=4.8.4 <6.1.0" -> TS 7 hard-refuses.
|
||||
- dependency-name: "typescript"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# @tsparticles v4 changed its exported API; the vendored SparklesCore from the
|
||||
# Aceternity registry targets the v3 API (initParticlesEngine, IEffect.fill...).
|
||||
# NOTE: dependabot `ignore` does NOT support wildcards (only `groups` does) —
|
||||
# list each exact scoped package.
|
||||
- dependency-name: "@tsparticles/react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/engine"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/slim"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-config-next majors follow Next.js majors and can pull incompatible
|
||||
# plugin versions; framework majors are deliberate, not auto-bumps.
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-plugin-react 8.x targets ESLint 10, which conflicts with the
|
||||
# pinned eslint 9 (and the vendored components' validated toolchain).
|
||||
- dependency-name: "eslint-plugin-react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns: ["*"]
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: lint + typecheck + test + build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.14"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Unit tests
|
||||
run: bun run test
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Format check
|
||||
run: bun run format:check
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# test tooling
|
||||
coverage
|
||||
.nyc_output
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
.playwright
|
||||
|
||||
# planning files
|
||||
.planning
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
bun run lint-staged
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
build
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
.git
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"tailwindStylesheet": "./app/globals.css"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
FROM oven/bun:1.3.14 AS base
|
||||
WORKDIR /app
|
||||
|
||||
# --- deps ---
|
||||
FROM base AS deps
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# --- builder ---
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN bun run build
|
||||
|
||||
# --- runner ---
|
||||
FROM base AS runner
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
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
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1 +1,136 @@
|
||||
# nextjs-template
|
||||
# Next.js Template
|
||||
|
||||
Production-ready Next.js 16 (App Router, Turbopack) starter on Bun, with
|
||||
shadcn/ui Base UI components, TypeScript strict, and a zero-warning
|
||||
lint/typecheck/test/build pipeline.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Framework:** Next.js 16.3 (App Router, Turbopack, standalone output)
|
||||
- **Runtime:** Bun 1.3.14
|
||||
- **UI:** shadcn/ui Base-Nova (Base UI) + Tailwind CSS v4, lucide-react, motion
|
||||
- **Data:** @tanstack/react-query, @tanstack/react-table
|
||||
- **Forms:** react-hook-form + zod (v4) + @hookform/resolvers
|
||||
- **Auth:** better-auth (memory adapter, swap to Drizzle/Postgres when ready)
|
||||
- **API client:** openapi-fetch + openapi-typescript, ky
|
||||
- **Markdown:** react-markdown + remark-gfm + shiki (server-side highlighting)
|
||||
- **Misc:** zustand, nuqs, sonner, cmdk, vaul, embla-carousel-react, date-fns,
|
||||
next-themes, recharts
|
||||
- **Motion effects:** 48 Aceternity UI effects vendored as source in
|
||||
`components/ui/` (motion + @tabler/icons-react + @tsparticles + simplex-noise),
|
||||
showcased live at `/ui`
|
||||
|
||||
## UI gallery (`/ui`)
|
||||
|
||||
48 Aceternity effects (3d-pin, bento-grid, sparkles, tracing-beam, …) vendored
|
||||
verbatim from the [Aceternity registry](https://ui.aceternity.com/components)
|
||||
and demoed live in `components/showcase/gallery.tsx`. The shadcn Base-Nova set
|
||||
(accordions, dialogs, tables, …) ships alongside in the same `components/ui/`
|
||||
dir — 110 files total.
|
||||
|
||||
| Rule | Detail |
|
||||
| ------------- | ----------------------------------------------------------------------- |
|
||||
| Source | Registry JSON, verbatim + `motion/react` / `cn` import fixes |
|
||||
| Rename | Aceternity tabs → `animated-tabs.tsx` (collides with shadcn `tabs.tsx`) |
|
||||
| Skipped | three.js trio, apple-cards-carousel (heavy deps, out of scope) |
|
||||
| Overlay demos | `MultiStepLoader` demo is state-gated — never hardcode `loading` |
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
bun install
|
||||
cp .env.example .env # optional: set BETTER_AUTH_SECRET / BETTER_AUTH_URL
|
||||
bun run dev # http://localhost:3000
|
||||
```
|
||||
|
||||
Requires Node ≥ 18 (Bun runtime) — see `packageManager` in package.json.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------- |
|
||||
| `bun run dev` | Start dev server (Turbopack, NODE_ENV pinned to development) |
|
||||
| `bun run build` | Production build (standalone output for Docker) |
|
||||
| `bun run start` | Serve the production build |
|
||||
| `bun run lint` | ESLint (next core-web-vitals + typescript) |
|
||||
| `bun run lint:fix` | ESLint with autofix |
|
||||
| `bun run typecheck` | `tsc --noEmit` (strict + noUnusedLocals/Parameters) |
|
||||
| `bun run test` | Vitest unit tests (jsdom + jest-dom) |
|
||||
| `bun run test:watch` | Vitest watch mode |
|
||||
| `bun run test:coverage` | Vitest with v8 coverage report |
|
||||
| `bun run test:e2e` | Playwright E2E (Chromium, dedicated :3100) |
|
||||
| `bun run format` / `format:check` | Prettier write / check (Tailwind v4 plugin) |
|
||||
| `bun run api:generate` | Regenerate `types/api.ts` from `/api/openapi` (dev server must run) |
|
||||
| `bun run prepare` | Install husky hooks |
|
||||
|
||||
Git hooks (husky + lint-staged) run ESLint + Prettier on staged files.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
app/
|
||||
page.tsx # landing page (HomeHero: Cover + Sparkles + Spotlight)
|
||||
ui/ # /ui gallery — 48 Aceternity effects, live demos
|
||||
demo/ # wiring example: markdown+shiki, form, table, session
|
||||
protected/ # route guarded by middleware (better-auth session)
|
||||
error.tsx / loading.tsx / not-found.tsx
|
||||
api/
|
||||
health/ # GET health JSON
|
||||
openapi/ # OpenAPI 3.1 spec for codegen
|
||||
auth/[...all]/ # better-auth handler
|
||||
items/ # GET list / POST create (zod-validated)
|
||||
components/
|
||||
providers.tsx # QueryClient + Theme + Nuqs + Tooltip + Sonner
|
||||
ui/ # shadcn Base-Nova components (CLI-managed)
|
||||
demo/ # form + table wiring examples
|
||||
hooks/ # use-mobile (useSyncExternalStore)
|
||||
lib/
|
||||
auth.ts # better-auth server (memory adapter)
|
||||
auth-client.ts # better-auth react client
|
||||
api.ts # typed openapi-fetch client
|
||||
http.ts # ky wrapper
|
||||
query-client.ts # react-query client (per-request on server)
|
||||
env.ts # zod-validated env (no import-time throw)
|
||||
types/api.ts # generated openapi-typescript types
|
||||
middleware.ts # auth route guard
|
||||
e2e/ # Playwright specs
|
||||
.github/workflows/ci.yml
|
||||
Dockerfile # multi-stage standalone build
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
better-auth is wired but intentionally runs on the in-memory adapter so the
|
||||
template builds without a database.
|
||||
|
||||
1. `bunx @better-auth/cli secret` → put the result in `BETTER_AUTH_SECRET`.
|
||||
2. Set `BETTER_AUTH_URL` (+ `NEXT_PUBLIC_BETTER_AUTH_URL` for the client).
|
||||
3. To persist users, swap `memoryAdapter({})` in `lib/auth.ts` for a real
|
||||
adapter (e.g. `drizzleAdapter(db, { provider: "pg" })`) and add the DB URL.
|
||||
|
||||
Routes under `/protected` require a session (see `middleware.ts`).
|
||||
|
||||
## OpenAPI + typed client
|
||||
|
||||
`app/api/openapi` serves an OpenAPI 3.1 document. Regenerate the typed client:
|
||||
|
||||
```bash
|
||||
bun run dev & # server must be up
|
||||
bun run api:generate # writes types/api.ts
|
||||
```
|
||||
|
||||
Then use `lib/api.ts` (`createClient<paths>` from openapi-fetch).
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker build -t nextjs-template .
|
||||
docker run -p 3000:3000 nextjs-template
|
||||
```
|
||||
|
||||
Multi-stage standalone image (~50MB runtime); runs as non-root user; requires
|
||||
NODE_ENV=production and the env vars above at runtime.
|
||||
|
||||
## License
|
||||
|
||||
Private template.
|
||||
@@ -0,0 +1,4 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
service: "nextjs-template",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createItemSchema, MOCK_ITEMS, type Item, type CreateItemOutput } from "@/lib/items";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ items: MOCK_ITEMS });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = createItemSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Validation failed",
|
||||
details: parsed.error.flatten().fieldErrors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const input: CreateItemOutput = parsed.data;
|
||||
const item: Item = {
|
||||
id: crypto.randomUUID(),
|
||||
...input,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// In a real app: persist to DB here. MOCK_ITEMS is module-scoped, so the
|
||||
// created item lives only for the lifetime of the server process.
|
||||
MOCK_ITEMS.unshift(item);
|
||||
|
||||
return NextResponse.json({ item }, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Minimal OpenAPI document describing the template's own API routes.
|
||||
* Kept in sync with app/api/** — regenerate client types with:
|
||||
* bun run api:generate
|
||||
*/
|
||||
export function GET() {
|
||||
return NextResponse.json({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Next.js Template API",
|
||||
version: "0.1.0",
|
||||
},
|
||||
paths: {
|
||||
"/api/health": {
|
||||
get: {
|
||||
operationId: "getHealth",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Service health",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
ok: { type: "boolean" },
|
||||
service: { type: "string" },
|
||||
timestamp: { type: "string", format: "date-time" },
|
||||
},
|
||||
required: ["ok", "service", "timestamp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/{action}": {
|
||||
parameters: [
|
||||
{
|
||||
name: "action",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
get: {
|
||||
operationId: "authAction",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Auth route (Better Auth handler)",
|
||||
content: { "application/json": { schema: { type: "object" } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeBlock } from "@/components/ui/code-block";
|
||||
import { DemoClient } from "@/components/demo/demo-client";
|
||||
|
||||
const SAMPLE_MD = `# Demo Page
|
||||
|
||||
This page shows the core wiring of the template:
|
||||
|
||||
- **Server component** renders this markdown via \`react-markdown\` + \`remark-gfm\`
|
||||
- **Shiki** highlights code blocks server-side
|
||||
- **React Query** fetches \`/api/items\`
|
||||
- **React Hook Form + Zod** validates the "Add item" form
|
||||
- **better-auth** session is read on the server below
|
||||
|
||||
## Code sample
|
||||
|
||||
\`\`\`ts
|
||||
export function getGreeting(name: string): string {
|
||||
return \`Hello, \${name}!\`;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
> Tip: copy this page as a starting point for real features.
|
||||
`;
|
||||
|
||||
export default function DemoPage() {
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-8 p-8">
|
||||
<article className="prose prose-sm dark:prose-invert">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code(props) {
|
||||
const { children, className, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className ?? "");
|
||||
if (match) {
|
||||
return (
|
||||
<CodeBlock
|
||||
code={String(children).replace(/\n$/, "")}
|
||||
lang={match[1]}
|
||||
className="my-4 overflow-x-auto rounded-lg border text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{SAMPLE_MD}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
<DemoClient />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<h1 className="text-2xl font-semibold">Something went wrong</h1>
|
||||
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||
{error.message || "An unexpected error occurred."}
|
||||
</p>
|
||||
{error.digest ? (
|
||||
<p className="font-mono text-xs text-muted-foreground">Error ID: {error.digest}</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => reset()}
|
||||
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+130
@@ -0,0 +1,130 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Providers } from "@/components/providers";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Next.js Template — Aceternity + shadcn",
|
||||
template: "%s — Next.js Template",
|
||||
},
|
||||
description:
|
||||
"Production-ready Next.js 16 starter on Bun with 48 vendored Aceternity UI effects, shadcn Base-Nova components, Tailwind v4, motion, better-auth and a typed API client.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="flex min-h-full flex-col">
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-8" role="status">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span className="size-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground" />
|
||||
Loading…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<p className="text-5xl font-bold text-muted-foreground">404</p>
|
||||
<h1 className="text-2xl font-semibold">Page not found</h1>
|
||||
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||
The page you are looking for does not exist or has been moved.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Back to home
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { HomeHero } from "@/components/home-hero";
|
||||
|
||||
export default function Home() {
|
||||
return <HomeHero />;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ProtectedPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex max-w-2xl flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<h1 className="text-2xl font-semibold">Protected Page</h1>
|
||||
{session ? (
|
||||
<div className="flex flex-col items-center gap-2 rounded-lg border p-6">
|
||||
<p className="text-lg">
|
||||
Welcome, <span className="font-medium">{session.user.name}</span>!{" "}
|
||||
<span className="text-muted-foreground">({session.user.email})</span>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Session expires: {new Date(session.session.expiresAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No session found.</p>
|
||||
)}
|
||||
<Link href="/" className="text-sm text-primary underline-offset-4 hover:underline">
|
||||
← Back home
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { ShowcaseGallery } from "@/components/showcase/gallery";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "UI Gallery — Aceternity + shadcn",
|
||||
description:
|
||||
"Forty-eight Aceternity UI effects vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router, alongside the full shadcn Base-Nova set.",
|
||||
};
|
||||
|
||||
const SECTIONS = [
|
||||
"3D Pin / Marquee",
|
||||
"Backgrounds & Beams",
|
||||
"Cards & Hover",
|
||||
"Text Effects",
|
||||
"Inputs & Buttons",
|
||||
];
|
||||
|
||||
export default function UiGalleryPage() {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_55%)]">
|
||||
<div className="w-full border-b border-zinc-200 bg-white/80 py-10 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/80">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-6">
|
||||
<nav className="text-xs font-medium tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||
<Link
|
||||
href="/"
|
||||
className="transition-colors hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<span className="px-2">/</span>
|
||||
<span className="text-zinc-800 dark:text-zinc-200">UI Gallery</span>
|
||||
</nav>
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-zinc-50">
|
||||
Aceternity UI gallery
|
||||
</h1>
|
||||
<p className="max-w-2xl text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
48 Aceternity effects vendored straight from the{" "}
|
||||
<a
|
||||
href="https://ui.aceternity.com/components"
|
||||
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Aceternity registry
|
||||
</a>{" "}
|
||||
and wired for this template. Everything below is live — hover, click and scroll.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{SECTIONS.map((s) => (
|
||||
<a
|
||||
key={s}
|
||||
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
|
||||
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
|
||||
>
|
||||
{s}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="w-full max-w-6xl flex-1 px-6 py-12">
|
||||
<Suspense
|
||||
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
|
||||
>
|
||||
<ShowcaseGallery />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ItemForm } from "@/components/demo/item-form";
|
||||
import { ItemList } from "@/components/demo/item-list";
|
||||
|
||||
export function DemoClient() {
|
||||
return (
|
||||
<section className="flex flex-col gap-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<ItemForm />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold">Items (react-query + react-table)</h2>
|
||||
<ItemList />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const itemSchema = z.object({
|
||||
name: z.string().min(1, "Name is required").max(100),
|
||||
quantity: z.coerce
|
||||
.number({ error: "Quantity must be a number" })
|
||||
.int("Must be a whole number")
|
||||
.min(0, "Minimum 0")
|
||||
.max(10000, "Maximum 10000"),
|
||||
});
|
||||
|
||||
type ItemFormValues = z.input<typeof itemSchema>;
|
||||
type ItemSubmitValues = z.output<typeof itemSchema>;
|
||||
|
||||
export function ItemForm() {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ItemFormValues, unknown, ItemSubmitValues>({
|
||||
resolver: zodResolver(itemSchema),
|
||||
defaultValues: { name: "", quantity: 1 },
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: ItemSubmitValues) => {
|
||||
const res = await fetch("/api/items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => null);
|
||||
throw new Error(err?.error ?? "Failed to create item");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Item created");
|
||||
reset();
|
||||
queryClient.invalidateQueries({ queryKey: ["items"] });
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit((values) => mutation.mutate(values))}
|
||||
className="flex flex-col gap-3 rounded-lg border p-4"
|
||||
noValidate
|
||||
>
|
||||
<h2 className="text-sm font-semibold">Add item (RHF + zod + react-query)</h2>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="text-xs font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
{...register("name")}
|
||||
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||
placeholder="e.g. USB-C cable"
|
||||
/>
|
||||
{errors.name && <span className="text-xs text-destructive">{errors.name.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="quantity" className="text-xs font-medium">
|
||||
Quantity
|
||||
</label>
|
||||
<input
|
||||
id="quantity"
|
||||
type="number"
|
||||
{...register("quantity")}
|
||||
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||
/>
|
||||
{errors.quantity && (
|
||||
<span className="text-xs text-destructive">{errors.quantity.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || mutation.isPending}
|
||||
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
{mutation.isPending ? "Creating…" : "Create item"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import {
|
||||
legacyCreateColumnHelper as createColumnHelper,
|
||||
getCoreRowModel,
|
||||
useLegacyTable,
|
||||
type LegacyColumnDef,
|
||||
} from "@tanstack/react-table/legacy";
|
||||
import { format } from "date-fns";
|
||||
import type { Item } from "@/lib/items";
|
||||
|
||||
const columnHelper = createColumnHelper<Item>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
}),
|
||||
columnHelper.accessor("quantity", {
|
||||
header: "Qty",
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created",
|
||||
cell: (info) => format(new Date(info.getValue<string>()), "MMM d, HH:mm"),
|
||||
}),
|
||||
] as LegacyColumnDef<Item>[];
|
||||
|
||||
export function ItemList() {
|
||||
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||
queryKey: ["items"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/items");
|
||||
if (!res.ok) throw new Error("Failed to load items");
|
||||
const body = await res.json();
|
||||
return body.items as Item[];
|
||||
},
|
||||
});
|
||||
|
||||
const table = useLegacyTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <p className="text-sm text-muted-foreground">Loading items…</p>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-sm text-destructive">
|
||||
<span>{error.message}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="rounded-md border px-2 py-1 hover:bg-muted"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 text-left">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id} className="px-4 py-2 font-medium">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className="border-t hover:bg-muted/30">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-4 py-2">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{data?.length === 0 ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">No items yet.</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "motion/react";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
|
||||
export function HomeHero() {
|
||||
return (
|
||||
<div className="relative flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-zinc-50 antialiased dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.06),transparent_60%)]">
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<Spotlight className="-top-40 left-0 md:-top-20 md:left-60" fill="white" />
|
||||
</div>
|
||||
|
||||
{/* Sparkles fill the hero background */}
|
||||
<div className="absolute inset-0 z-0 h-full w-full">
|
||||
<SparklesCore
|
||||
id="home-sparkles"
|
||||
background="transparent"
|
||||
minSize={0.6}
|
||||
maxSize={1.4}
|
||||
particleDensity={60}
|
||||
particleColor="#71717a"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.main
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||
className="relative z-10 flex w-full max-w-4xl flex-col items-center gap-8 px-6 py-24 text-center"
|
||||
>
|
||||
<span className="rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-zinc-500 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-400">
|
||||
Next.js 16 · Tailwind v4 · motion · 48 Aceternity effects + shadcn
|
||||
</span>
|
||||
|
||||
<h1 className="max-w-3xl text-4xl leading-tight font-semibold tracking-tight text-zinc-900 sm:text-6xl dark:text-white">
|
||||
A template that ships with <Cover>animated UI out of the box.</Cover>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base leading-7 text-zinc-600 dark:text-zinc-300">
|
||||
Aceternity components vendored as source, Tailwind v4 native, zero-warning lint. Every
|
||||
effect below is live — build a landing page without starting from scratch.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<Link href="/ui" passHref>
|
||||
<MagneticButton strength={0.5} maxDistance={60}>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200">
|
||||
Browse the gallery
|
||||
<span aria-hidden>→</span>
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Link>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-zinc-300 px-6 py-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
|
||||
>
|
||||
View the demo wiring
|
||||
</Link>
|
||||
</div>
|
||||
</motion.main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { getQueryClient } from "@/lib/query-client";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = React.useMemo(() => getQueryClient(), []);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<NuqsAdapter>
|
||||
<TooltipProvider delay={0}>
|
||||
{children}
|
||||
<Toaster richColors closeButton position="bottom-right" />
|
||||
</TooltipProvider>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "motion/react";
|
||||
import { AuroraBackground } from "@/components/ui/aurora-background";
|
||||
import { BackgroundBeams } from "@/components/ui/background-beams";
|
||||
import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation";
|
||||
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
|
||||
import { CardSpotlight } from "@/components/ui/card-spotlight";
|
||||
import { CardStack } from "@/components/ui/card-stack";
|
||||
import { DirectionAwareHover } from "@/components/ui/direction-aware-hover";
|
||||
import { EncryptedText } from "@/components/ui/encrypted-text";
|
||||
import { EvervaultCard } from "@/components/ui/evervault-card";
|
||||
import { FlipWords } from "@/components/ui/flip-words";
|
||||
import { HeroHighlight, Highlight } from "@/components/ui/hero-highlight";
|
||||
import { InfiniteMovingCards } from "@/components/ui/infinite-moving-cards";
|
||||
import { MovingBorder } from "@/components/ui/moving-border";
|
||||
import { PinContainer } from "@/components/ui/3d-pin";
|
||||
import { PlaceholdersAndVanishInput } from "@/components/ui/placeholders-and-vanish-input";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
import { ThreeDMarquee } from "@/components/ui/3d-marquee";
|
||||
import { TracingBeam } from "@/components/ui/tracing-beam";
|
||||
import { TypewriterEffect } from "@/components/ui/typewriter-effect";
|
||||
import { AnimatedTooltip } from "@/components/ui/animated-tooltip";
|
||||
import { Tabs } from "@/components/ui/animated-tabs";
|
||||
import { WavyBackground } from "@/components/ui/wavy-background";
|
||||
import { Boxes } from "@/components/ui/background-boxes";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { TextGenerateEffect } from "@/components/ui/text-generate-effect";
|
||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||
import { SquigglyText } from "@/components/ui/squiggly-text";
|
||||
import {
|
||||
TextRevealCard,
|
||||
TextRevealCardTitle,
|
||||
TextRevealCardDescription,
|
||||
} from "@/components/ui/text-reveal-card";
|
||||
import { Meteors } from "@/components/ui/meteors";
|
||||
import { GlareCard } from "@/components/ui/glare-card";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { GooeyInput } from "@/components/ui/gooey-input";
|
||||
import { Notch } from "@/components/ui/notch";
|
||||
import { FloatingDock } from "@/components/ui/floating-dock";
|
||||
import { HeroParallax } from "@/components/ui/hero-parallax";
|
||||
import { ParallaxScroll } from "@/components/ui/parallax-scroll";
|
||||
import { ContainerScroll } from "@/components/ui/container-scroll-animation";
|
||||
import { StickyScroll } from "@/components/ui/sticky-scroll-reveal";
|
||||
import { Timeline } from "@/components/ui/timeline";
|
||||
import { FocusCards } from "@/components/ui/focus-cards";
|
||||
import { Tooltip } from "@/components/ui/tooltip-card";
|
||||
import { HoverEffect } from "@/components/ui/card-hover-effect";
|
||||
import { BackgroundGradient } from "@/components/ui/background-gradient";
|
||||
import { MultiStepLoader } from "@/components/ui/multi-step-loader";
|
||||
import {
|
||||
ModalProvider,
|
||||
Modal,
|
||||
ModalTrigger,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@/components/ui/animated-modal";
|
||||
import { MaskContainer } from "@/components/ui/svg-mask-effect";
|
||||
import { GoogleGeminiEffect } from "@/components/ui/google-gemini-effect";
|
||||
import { IconHome, IconUser, IconSettings, IconMoon, IconSun } from "@tabler/icons-react";
|
||||
import { useScroll, useTransform, MotionValue } from "motion/react";
|
||||
import { useState } from "react";
|
||||
|
||||
function MultiStepLoaderDemo() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<MultiStepLoader
|
||||
loading={loading}
|
||||
loadingStates={[
|
||||
{ text: "Fetching" },
|
||||
{ text: "Compiling" },
|
||||
{ text: "Rendering" },
|
||||
{ text: "Done" },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 4000);
|
||||
}}
|
||||
className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{loading ? "Loading…" : "Run loader"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-col items-center justify-center gap-5 overflow-hidden rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{label && (
|
||||
<span className="absolute right-3 bottom-3 rounded-full border border-zinc-200 bg-zinc-50 px-2 py-0.5 text-[10px] font-medium text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionLabel({ id, index, title }: { id: string; index: string; title: string }) {
|
||||
return (
|
||||
<div id={id} className="flex w-full max-w-6xl flex-col items-start gap-1.5">
|
||||
<span className="text-[11px] font-semibold tracking-[0.2em] text-zinc-400 uppercase">
|
||||
{index}
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BENTO_ITEMS = [
|
||||
{
|
||||
title: "Vendored, not installed",
|
||||
description: "Each component ships as source in components/ui — zero registry lock-in.",
|
||||
header: "✦",
|
||||
className: "md:col-span-2",
|
||||
},
|
||||
{
|
||||
title: "Tailwind v4 native",
|
||||
description: "Same cn() util and zinc palette as the rest of the template.",
|
||||
header: "❖",
|
||||
},
|
||||
{
|
||||
title: "Zero-warning lint",
|
||||
description: "Vendored dir has its own scoped eslint override.",
|
||||
header: "✓",
|
||||
className: "md:col-span-2",
|
||||
},
|
||||
];
|
||||
|
||||
const PEOPLE = [
|
||||
{ id: 1, name: "Aceternity", designation: "ui.aceternity.com", image: "/vercel.svg" },
|
||||
{ id: 2, name: "Vercel", designation: "nextjs-template", image: "/next.svg" },
|
||||
{ id: 3, name: "Template", designation: "Shadcn + Aceternity", image: "/vercel.svg" },
|
||||
];
|
||||
|
||||
const STACK_CARDS = [
|
||||
{
|
||||
id: 1,
|
||||
name: "3D Pin",
|
||||
designation: "Hover cards",
|
||||
content: <>A card that lifts on hover, with a beam behind it.</>,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Tracing Beam",
|
||||
designation: "Scroll guide",
|
||||
content: <>A beam that traces your scroll, guiding the eye down the page.</>,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Evervault",
|
||||
designation: "Encrypted hover",
|
||||
content: <>Patterns react to your mouse while the card sits above an animated capsule.</>,
|
||||
},
|
||||
];
|
||||
|
||||
const TESTIMONIALS_MOCK = [
|
||||
{
|
||||
quote: "The fifty components slot straight into the template and just work.",
|
||||
name: "Next.js Template",
|
||||
title: "Vendored showcase",
|
||||
},
|
||||
{
|
||||
quote: "Hover, scroll and type — everything below is live, not a static screenshot.",
|
||||
name: "Aceternity Registry",
|
||||
title: "ui.aceternity.com",
|
||||
},
|
||||
{
|
||||
quote: "Built with motion/react and Tailwind v4, matching the template's own stack.",
|
||||
name: "Motion",
|
||||
title: "motion/react",
|
||||
},
|
||||
];
|
||||
|
||||
export function ShowcaseGallery() {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-20 py-10">
|
||||
<section id="hero" className="flex w-full flex-col items-center gap-6">
|
||||
<AuroraBackground className="w-full overflow-hidden rounded-3xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex flex-col items-center gap-4 px-6 py-24 text-center"
|
||||
>
|
||||
<p className="text-xs font-medium tracking-widest text-zinc-500 uppercase">
|
||||
50 components · vendored · lint-clean
|
||||
</p>
|
||||
<h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl dark:text-white">
|
||||
Aceternity UI, wired for this template
|
||||
</h1>
|
||||
<p className="max-w-xl text-sm leading-6 text-zinc-600 dark:text-zinc-300">
|
||||
Animated cards, beams, bento grids and text effects — pulled from the registry,
|
||||
adapted to Tailwind v4 + motion/react, and demoed live on this page.
|
||||
</p>
|
||||
</motion.div>
|
||||
</AuroraBackground>
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="3d-marquee">
|
||||
<ThreeDMarquee
|
||||
className="w-full"
|
||||
images={["/next.svg", "/vercel.svg", "/next.svg", "/vercel.svg"]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="animated-tooltip">
|
||||
<AnimatedTooltip items={PEOPLE} />
|
||||
</Card>
|
||||
<Card label="typewriter-effect">
|
||||
<TypewriterEffect
|
||||
words={[
|
||||
{ text: "Typewriter" },
|
||||
{ text: "effect" },
|
||||
{ text: "…" },
|
||||
{ text: "works." },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="beams" index="01" title="Beams, sparkles & gradients" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="background-beams" className="md:col-span-2">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl">
|
||||
<BackgroundBeams className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">
|
||||
BackgroundBeams — hover to see the glow
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="sparkles">
|
||||
<div className="flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<SparklesCore
|
||||
background="transparent"
|
||||
minSize={0.6}
|
||||
maxSize={1.6}
|
||||
particleDensity={90}
|
||||
particleColor="#a1a1aa"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
<p className="absolute bottom-4 text-xs text-zinc-500">SparklesCore</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="background-gradient-animation" className="md:col-span-3">
|
||||
<BackgroundGradientAnimation
|
||||
containerClassName="rounded-xl"
|
||||
className="absolute inset-0 z-20"
|
||||
>
|
||||
<div className="relative z-30 flex flex-col items-center gap-2 px-6 text-center">
|
||||
<p className="text-lg font-semibold text-white">BackgroundGradientAnimation</p>
|
||||
<p className="text-xs text-white/70">Animated aurora wash behind any content</p>
|
||||
</div>
|
||||
</BackgroundGradientAnimation>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="cards" index="02" title="Pins, stacks & hover cards" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="3d-pin" className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<PinContainer title="ui.aceternity.com" href="#cards">
|
||||
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">Hover me</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">3D Pin</p>
|
||||
</div>
|
||||
</PinContainer>
|
||||
<PinContainer title="bento grid →" href="#bento">
|
||||
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
|
||||
Perspective lift
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">on hover</p>
|
||||
</div>
|
||||
</PinContainer>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="card-spotlight">
|
||||
<CardSpotlight className="w-full p-8">
|
||||
<p className="text-sm font-medium text-zinc-100">CardSpotlight</p>
|
||||
<p className="mt-2 text-xs text-zinc-400">Move your cursor for a spotlight.</p>
|
||||
</CardSpotlight>
|
||||
</Card>
|
||||
<Card label="card-stack">
|
||||
<CardStack items={STACK_CARDS} />
|
||||
</Card>
|
||||
<Card label="direction-aware-hover">
|
||||
<DirectionAwareHover imageUrl="/next.svg" className="w-full">
|
||||
<p className="text-sm font-medium text-white">DirectionAwareHover</p>
|
||||
</DirectionAwareHover>
|
||||
</Card>
|
||||
<Card label="bento-grid">
|
||||
<BentoGrid className="mx-auto w-full max-w-3xl">
|
||||
{BENTO_ITEMS.map((item, i) => (
|
||||
<BentoGridItem
|
||||
key={i}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
header={item.header}
|
||||
className={item.className}
|
||||
/>
|
||||
))}
|
||||
</BentoGrid>
|
||||
</Card>
|
||||
<Card label="moving-border">
|
||||
<MovingBorder
|
||||
duration={3000}
|
||||
className="rounded-full border border-zinc-200 bg-white px-6 py-2 text-sm font-medium text-zinc-800 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
|
||||
>
|
||||
MovingBorder
|
||||
</MovingBorder>
|
||||
</Card>
|
||||
<Card label="infinite-moving-cards">
|
||||
<InfiniteMovingCards
|
||||
items={TESTIMONIALS_MOCK}
|
||||
direction="left"
|
||||
speed="slow"
|
||||
pauseOnHover
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="text" index="03" title="Highlight, flip & encrypt" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="hero-highlight" className="md:col-span-2">
|
||||
<HeroHighlight containerClassName="w-full rounded-xl">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="px-4 py-3 text-center font-sans text-2xl text-zinc-900 sm:text-3xl dark:text-white"
|
||||
>
|
||||
Text effects with{" "}
|
||||
<Highlight className="text-black dark:text-white">mouse-follow highlight</Highlight>
|
||||
</motion.h1>
|
||||
</HeroHighlight>
|
||||
</Card>
|
||||
<Card label="flip-words">
|
||||
<div className="text-center text-lg text-zinc-700 dark:text-zinc-200">
|
||||
Animate text like <FlipWords words={["sparkles", "beams", "pins", "marquees"]} />
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="encrypted-text">
|
||||
<EncryptedText
|
||||
text="Aceternity, vendored."
|
||||
className="text-center text-lg text-zinc-800 dark:text-zinc-100"
|
||||
/>
|
||||
</Card>
|
||||
<Card label="evervault-card" className="md:col-span-2">
|
||||
<div className="w-full max-w-md">
|
||||
<EvervaultCard text="hover the capsule" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="tracing-beam" className="md:col-span-2">
|
||||
<TracingBeam className="w-full px-2">
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{[
|
||||
["Beam one", "The beam follows your scroll."],
|
||||
["Beam two", "It traces between sticky sections."],
|
||||
["Beam three", "Pairs perfectly with tall content."],
|
||||
].map(([t, d]) => (
|
||||
<div
|
||||
key={t}
|
||||
className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/50"
|
||||
>
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">{t}</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">{d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TracingBeam>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="inputs" index="04" title="Inputs with built-in motion" />
|
||||
<Card label="placeholders-and-vanish-input" className="max-w-2xl">
|
||||
<PlaceholdersAndVanishInput
|
||||
placeholders={["Search ", "Try a phrase…", "The placeholder vanishes on focus"]}
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="beams2" index="05" title="Waves, boxes & spotlights" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="wavy-background" className="md:col-span-2">
|
||||
<WavyBackground className="w-full rounded-xl py-16">
|
||||
<p className="relative z-10 text-2xl font-bold text-white">WavyBackground</p>
|
||||
<p className="relative z-10 text-sm text-zinc-300">
|
||||
Animated SVG waves behind content.
|
||||
</p>
|
||||
</WavyBackground>
|
||||
</Card>
|
||||
<Card label="background-boxes">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Boxes className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">
|
||||
Boxes — grid dots pattern
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="spotlight">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Spotlight className="-top-20 left-0" fill="white" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-200">Spotlight</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="background-gradient" className="md:col-span-2">
|
||||
<BackgroundGradient className="rounded-xl p-6">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
|
||||
BackgroundGradient — animated border glow
|
||||
</p>
|
||||
</BackgroundGradient>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="text2" index="06" title="More text effects" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="cover">
|
||||
<p className="text-center text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
Outline <Cover>Cover</Cover> underline
|
||||
</p>
|
||||
</Card>
|
||||
<Card label="text-generate-effect">
|
||||
<TextGenerateEffect words="Text that generates itself, character by character." />
|
||||
</Card>
|
||||
<Card label="text-hover-effect">
|
||||
<TextHoverEffect text="HOVER" />
|
||||
</Card>
|
||||
<Card label="squiggly-text">
|
||||
<SquigglyText className="text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
SquigglyText wobble
|
||||
</SquigglyText>
|
||||
</Card>
|
||||
<Card label="text-reveal-card" className="md:col-span-2">
|
||||
<TextRevealCard text="Hover to reveal" revealText="A secret message 👀">
|
||||
<TextRevealCardTitle>TextRevealCard</TextRevealCardTitle>
|
||||
<TextRevealCardDescription>
|
||||
The card's cyan gradient sweeps in on hover.
|
||||
</TextRevealCardDescription>
|
||||
</TextRevealCard>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="cards2" index="07" title="Cards, meteors & buttons" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="meteors">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Meteors number={20} className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">Meteors</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="glare-card">
|
||||
<GlareCard className="flex h-full min-h-[240px] items-center justify-center rounded-xl">
|
||||
<p className="text-sm font-medium text-white">GlareCard</p>
|
||||
</GlareCard>
|
||||
</Card>
|
||||
<Card label="magnetic-button">
|
||||
<MagneticButton>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
MagneticButton
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="interaction" index="08" title="Inputs, docks & modals" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="gooey-input">
|
||||
<GooeyInput placeholder="Type to search…" />
|
||||
</Card>
|
||||
<Card label="notch">
|
||||
<Notch
|
||||
items={[
|
||||
{
|
||||
id: "theme",
|
||||
label: "Theme",
|
||||
options: [
|
||||
{ id: "light", label: "Light", icon: <IconSun className="h-4 w-4" /> },
|
||||
{ id: "dark", label: "Dark", icon: <IconMoon className="h-4 w-4" /> },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="floating-dock" className="md:col-span-2">
|
||||
<FloatingDock
|
||||
items={[
|
||||
{ title: "Home", icon: <IconHome className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Profile", icon: <IconUser className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Settings", icon: <IconSettings className="h-5 w-5" />, href: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="animated-modal" className="md:col-span-2">
|
||||
<ModalProvider>
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<span className="cursor-pointer rounded-full bg-zinc-900 px-5 py-2 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
Open animated modal
|
||||
</span>
|
||||
</ModalTrigger>
|
||||
<ModalBody>
|
||||
<ModalContent>
|
||||
<p className="text-center text-2xl text-zinc-900 dark:text-white">
|
||||
Animated Modal
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
The modal scales and fades, with a fancy backdrop.
|
||||
</p>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button className="rounded-full bg-zinc-900 px-5 py-2 text-sm text-white dark:bg-white dark:text-zinc-900">
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</ModalProvider>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="scroll" index="09" title="Scroll & parallax" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="sticky-scroll-reveal">
|
||||
<StickyScroll
|
||||
content={[
|
||||
{
|
||||
title: "StickyScroll",
|
||||
description: "Content sticks while you scroll; titles swap.",
|
||||
},
|
||||
{
|
||||
title: "Second block",
|
||||
description: "The next item rises as the previous fades.",
|
||||
},
|
||||
{
|
||||
title: "Third block",
|
||||
description: "Great for feature lists or onboarding.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="container-scroll-animation">
|
||||
<ContainerScroll
|
||||
titleComponent={
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">ContainerScroll</p>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-xl bg-zinc-100 p-8 dark:bg-zinc-800">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
The card scales up as you scroll past.
|
||||
</p>
|
||||
</div>
|
||||
</ContainerScroll>
|
||||
</Card>
|
||||
<Card label="timeline">
|
||||
<Timeline
|
||||
data={[
|
||||
{
|
||||
title: "2026 · Phase 1",
|
||||
content: <>Vendor registry components into the template.</>,
|
||||
},
|
||||
{ title: "2026 · Phase 2", content: <>Showcase gallery + home hero upgrade.</> },
|
||||
{
|
||||
title: "2026 · Phase 3",
|
||||
content: <>Zero-warning toolchain, green CI, auto-deploy.</>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="hover-scroll" index="10" title="Hover cards, parallax & masking" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="card-hover-effect">
|
||||
<HoverEffect
|
||||
className="w-full"
|
||||
items={[
|
||||
{ title: "HoverEffect", description: "Cards lift & glow on hover.", link: "#" },
|
||||
{ title: "Second card", description: "The grid is responsive.", link: "#" },
|
||||
{ title: "Third card", description: "Click to follow the link.", link: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="focus-cards">
|
||||
<FocusCards
|
||||
cards={[
|
||||
{ title: "FocusCards", src: "/vercel.svg" },
|
||||
{ title: "Second", src: "/next.svg" },
|
||||
{ title: "Third", src: "/vercel.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="tooltip-card">
|
||||
<Tooltip content={<p className="text-xs text-zinc-300">Tooltip on hover</p>}>
|
||||
<span className="cursor-pointer text-sm font-medium text-zinc-700 dark:text-zinc-200">
|
||||
Hover this for a tooltip
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
<Card label="multi-step-loader">
|
||||
<MultiStepLoaderDemo />
|
||||
</Card>
|
||||
<Card label="svg-mask-effect">
|
||||
<MaskContainer
|
||||
revealText={<p className="text-4xl font-bold text-zinc-300">Revealed!</p>}
|
||||
className="h-[16rem]"
|
||||
>
|
||||
<p className="text-4xl font-bold text-zinc-900 dark:text-white">
|
||||
Hover over this masked block
|
||||
</p>
|
||||
</MaskContainer>
|
||||
</Card>
|
||||
<Card label="hero-parallax">
|
||||
<HeroParallax
|
||||
products={[
|
||||
{ title: "Next", link: "#", thumbnail: "/next.svg" },
|
||||
{ title: "Vercel", link: "#", thumbnail: "/vercel.svg" },
|
||||
{ title: "Globe", link: "#", thumbnail: "/globe.svg" },
|
||||
{ title: "Window", link: "#", thumbnail: "/window.svg" },
|
||||
{ title: "File", link: "#", thumbnail: "/file.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="parallax-scroll">
|
||||
<ParallaxScroll
|
||||
images={[
|
||||
"/next.svg",
|
||||
"/vercel.svg",
|
||||
"/globe.svg",
|
||||
"/window.svg",
|
||||
"/file.svg",
|
||||
"/next.svg",
|
||||
]}
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="advanced" index="11" title="Tabs & gemini" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="animated-tabs">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
title: "Overview",
|
||||
value: "overview",
|
||||
content: <p className="p-4 text-sm">Overview tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Details",
|
||||
value: "details",
|
||||
content: <p className="p-4 text-sm">Details tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
value: "settings",
|
||||
content: <p className="p-4 text-sm">Settings tab</p>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<GeminiDemo />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GeminiDemo() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const pathLengths: MotionValue<number>[] = [
|
||||
useTransform(scrollYProgress, [0, 0.2], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.2, 0.4], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.4, 0.6], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.6, 0.8], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.8, 1], [0, 1]),
|
||||
];
|
||||
return (
|
||||
<Card label="google-gemini-effect" className="overflow-visible">
|
||||
<GoogleGeminiEffect
|
||||
pathLengths={pathLengths}
|
||||
title="Google Gemini Effect"
|
||||
description="Scroll — the SVG paths draw as you scroll."
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
export const ThreeDMarquee = ({ images, className }: { images: string[]; className?: string }) => {
|
||||
// Split the images array into 4 equal parts
|
||||
const chunkSize = Math.ceil(images.length / 4);
|
||||
const chunks = Array.from({ length: 4 }, (_, colIndex) => {
|
||||
const start = colIndex * chunkSize;
|
||||
return images.slice(start, start + chunkSize);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={cn("mx-auto block h-[600px] overflow-hidden rounded-2xl max-sm:h-100", className)}
|
||||
>
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<div className="size-[1720px] shrink-0 scale-50 sm:scale-75 lg:scale-100">
|
||||
<div
|
||||
style={{
|
||||
transform: "rotateX(55deg) rotateY(0deg) rotateZ(-45deg)",
|
||||
}}
|
||||
className="relative top-96 right-[50%] grid size-full origin-top-left grid-cols-4 gap-8 transform-3d"
|
||||
>
|
||||
{chunks.map((subarray, colIndex) => (
|
||||
<motion.div
|
||||
animate={{ y: colIndex % 2 === 0 ? 100 : -100 }}
|
||||
transition={{
|
||||
duration: colIndex % 2 === 0 ? 10 : 15,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
key={colIndex + "marquee"}
|
||||
className="flex flex-col items-start gap-8"
|
||||
>
|
||||
<GridLineVertical className="-left-4" offset="80px" />
|
||||
{subarray.map((image, imageIndex) => (
|
||||
<div className="relative" key={imageIndex + image}>
|
||||
<GridLineHorizontal className="-top-4" offset="20px" />
|
||||
<motion.img
|
||||
whileHover={{
|
||||
y: -10,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
key={imageIndex + image}
|
||||
src={image}
|
||||
alt={`Image ${imageIndex + 1}`}
|
||||
className="aspect-[970/700] rounded-lg object-cover ring ring-gray-950/5 hover:shadow-2xl"
|
||||
width={970}
|
||||
height={700}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineHorizontal = ({ className, offset }: { className?: string; offset?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "1px",
|
||||
"--width": "5px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "200px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute left-[calc(var(--offset)/2*-1)] h-[var(--height)] w-[calc(100%+var(--offset))]",
|
||||
"bg-[linear-gradient(to_right,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_left,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_right,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_right,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "5px",
|
||||
"--width": "1px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "150px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
|
||||
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const PinContainer = ({
|
||||
children,
|
||||
title,
|
||||
href,
|
||||
className,
|
||||
containerClassName,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
href?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}) => {
|
||||
const [transform, setTransform] = useState("translate(-50%,-50%) rotateX(0deg)");
|
||||
|
||||
const onMouseEnter = () => {
|
||||
setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)");
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn("group/pin relative z-50 cursor-pointer", containerClassName)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
href={href || "/"}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
transform: "rotateX(70deg) translateZ(0deg)",
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: transform,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 flex items-start justify-start overflow-hidden rounded-2xl border border-white/[0.1] bg-black p-4 shadow-[0_8px_16px_rgb(0_0_0/0.4)] transition duration-700 group-hover/pin:border-white/[0.2]"
|
||||
>
|
||||
<div className={cn("relative z-50", className)}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<PinPerspective title={title} href={href} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const PinPerspective = ({ title, href }: { title?: string; href?: string }) => {
|
||||
return (
|
||||
<motion.div className="pointer-events-none z-[60] flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100">
|
||||
<div className="inset-0 -mt-7 h-full w-full flex-none">
|
||||
<div className="absolute inset-x-0 top-0 flex justify-center">
|
||||
<a
|
||||
href={href}
|
||||
target={"_blank"}
|
||||
className="relative z-10 flex items-center space-x-2 rounded-full bg-zinc-950 px-4 py-0.5 ring-1 ring-white/10"
|
||||
>
|
||||
<span className="relative z-20 inline-block py-0.5 text-xs font-bold text-white">
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-emerald-400/90 to-emerald-400/0 transition-opacity duration-500 group-hover/btn:opacity-40"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
transform: "rotateX(70deg) translateZ(0)",
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 0,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 2,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 4,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 blur-[2px] group-hover/pin:h-40" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 group-hover/pin:h-40" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-cyan-600 blur-[3px]" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-cyan-300" />
|
||||
</>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
|
||||
import { cn } from "cn";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className={cn("flex w-full flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("not-last:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:texLine truncated
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon
|
||||
data-slot="accordion-trigger-icon"
|
||||
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
|
||||
/>
|
||||
<ChevronUpIcon
|
||||
data-slot="accordion-trigger-icon"
|
||||
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
|
||||
return (
|
||||
<AccordionPrimitive.Panel
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
||||
import { cn } from "cn";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
|
||||
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
|
||||
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Backdrop
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AlertDialogPrimitive.Popup.Props & {
|
||||
size?: "default" | "sm";
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Popup
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
|
||||
return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...props
|
||||
}: AlertDialogPrimitive.Close.Props &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Close
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
render={<Button variant={variant} size={size} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "cn";
|
||||
|
||||
const alertVariants = cva(
|
||||
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
interface ModalContextType {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
||||
|
||||
export const ModalProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <ModalContext.Provider value={{ open, setOpen }}>{children}</ModalContext.Provider>;
|
||||
};
|
||||
|
||||
export const useModal = () => {
|
||||
const context = useContext(ModalContext);
|
||||
if (!context) {
|
||||
throw new Error("useModal must be used within a ModalProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export function Modal({ children }: { children: ReactNode }) {
|
||||
return <ModalProvider>{children}</ModalProvider>;
|
||||
}
|
||||
|
||||
export const ModalTrigger = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { setOpen } = useModal();
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-md px-4 py-2 text-center text-black dark:text-white",
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalBody = ({ children, className }: { children: ReactNode; className?: string }) => {
|
||||
const { open } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const { setOpen } = useModal();
|
||||
useOutsideClick(modalRef, () => setOpen(false));
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
backdropFilter: "blur(0px)",
|
||||
}}
|
||||
className="fixed inset-0 z-50 flex h-full w-full items-center justify-center [perspective:800px] [transform-style:preserve-3d]"
|
||||
>
|
||||
<Overlay />
|
||||
|
||||
<motion.div
|
||||
ref={modalRef}
|
||||
className={cn(
|
||||
"relative z-50 flex max-h-[90%] min-h-[50%] flex-1 flex-col overflow-hidden border border-transparent bg-white md:max-w-[40%] md:rounded-2xl dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className
|
||||
)}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
rotateX: 40,
|
||||
y: 40,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotateX: 0,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
rotateX: 10,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 15,
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalContent = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return <div className={cn("flex flex-1 flex-col p-8 md:p-10", className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export const ModalFooter = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex justify-end bg-gray-100 p-4 dark:bg-neutral-900", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Overlay = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
backdropFilter: "blur(0px)",
|
||||
}}
|
||||
className={`bg-opacity-50 fixed inset-0 z-50 h-full w-full bg-black ${className}`}
|
||||
></motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const CloseIcon = () => {
|
||||
const { setOpen } = useModal();
|
||||
return (
|
||||
<button onClick={() => setOpen(false)} className="group absolute top-4 right-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-black transition duration-200 group-hover:scale-125 group-hover:rotate-3 dark:text-white"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M18 6l-12 12" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to detect clicks outside of a component.
|
||||
// Add it in a separate file, I've added here for simplicity
|
||||
export const useOutsideClick = (
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
callback: (event: MouseEvent | TouchEvent) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
// DO NOTHING if the element being clicked is the target element or their children
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", listener);
|
||||
document.addEventListener("touchstart", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
document.removeEventListener("touchstart", listener);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Tab = {
|
||||
title: string;
|
||||
value: string;
|
||||
content?: string | React.ReactNode | any;
|
||||
};
|
||||
|
||||
export const Tabs = ({
|
||||
tabs: propTabs,
|
||||
containerClassName,
|
||||
activeTabClassName,
|
||||
tabClassName,
|
||||
contentClassName,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
containerClassName?: string;
|
||||
activeTabClassName?: string;
|
||||
tabClassName?: string;
|
||||
contentClassName?: string;
|
||||
}) => {
|
||||
const [active, setActive] = useState<Tab>(propTabs[0]);
|
||||
const [tabs, setTabs] = useState<Tab[]>(propTabs);
|
||||
|
||||
const moveSelectedTabToTop = (idx: number) => {
|
||||
const newTabs = [...propTabs];
|
||||
const selectedTab = newTabs.splice(idx, 1);
|
||||
newTabs.unshift(selectedTab[0]);
|
||||
setTabs(newTabs);
|
||||
setActive(newTabs[0]);
|
||||
};
|
||||
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"no-visible-scrollbar relative flex w-full max-w-full flex-row items-center justify-start overflow-auto [perspective:1000px] sm:overflow-visible",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
{propTabs.map((tab, idx) => (
|
||||
<button
|
||||
key={tab.title}
|
||||
onClick={() => {
|
||||
moveSelectedTabToTop(idx);
|
||||
}}
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={cn("relative rounded-full px-4 py-2", tabClassName)}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
}}
|
||||
>
|
||||
{active.value === tab.value && (
|
||||
<motion.div
|
||||
layoutId="clickedbutton"
|
||||
transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-full bg-gray-200 dark:bg-zinc-800",
|
||||
activeTabClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="relative block text-black dark:text-white">{tab.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<FadeInDiv
|
||||
tabs={tabs}
|
||||
active={active}
|
||||
key={active.value}
|
||||
hovering={hovering}
|
||||
className={cn("mt-32", contentClassName)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FadeInDiv = ({
|
||||
className,
|
||||
tabs,
|
||||
hovering,
|
||||
}: {
|
||||
className?: string;
|
||||
key?: string;
|
||||
tabs: Tab[];
|
||||
active: Tab;
|
||||
hovering?: boolean;
|
||||
}) => {
|
||||
const isActive = (tab: Tab) => {
|
||||
return tab.value === tabs[0].value;
|
||||
};
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{tabs.map((tab, idx) => (
|
||||
<motion.div
|
||||
key={tab.value}
|
||||
layoutId={tab.value}
|
||||
style={{
|
||||
scale: 1 - idx * 0.1,
|
||||
top: hovering ? idx * -50 : 0,
|
||||
zIndex: -idx,
|
||||
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
|
||||
}}
|
||||
animate={{
|
||||
y: isActive(tab) ? [0, 40, 0] : 0,
|
||||
}}
|
||||
className={cn("absolute top-0 left-0 h-full w-full", className)}
|
||||
>
|
||||
{tab.content}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { motion, useTransform, AnimatePresence, useMotionValue, useSpring } from "motion/react";
|
||||
|
||||
export const AnimatedTooltip = ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
id: number;
|
||||
name: string;
|
||||
designation: string;
|
||||
image: string;
|
||||
}[];
|
||||
}) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const springConfig = { stiffness: 100, damping: 15 };
|
||||
const x = useMotionValue(0);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const rotate = useSpring(useTransform(x, [-100, 100], [-45, 45]), springConfig);
|
||||
const translateX = useSpring(useTransform(x, [-100, 100], [-50, 50]), springConfig);
|
||||
|
||||
const handleMouseMove = (event: any) => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
const halfWidth = event.target.offsetWidth / 2;
|
||||
x.set(event.nativeEvent.offsetX - halfWidth);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
className="group relative -mr-4"
|
||||
key={item.name}
|
||||
onMouseEnter={() => setHoveredIndex(item.id)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hoveredIndex === item.id && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 10,
|
||||
},
|
||||
}}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||
style={{
|
||||
translateX: translateX,
|
||||
rotate: rotate,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
className="absolute -top-16 left-1/2 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-black px-4 py-2 text-xs shadow-xl"
|
||||
>
|
||||
<div className="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-gradient-to-r from-transparent via-emerald-500 to-transparent" />
|
||||
<div className="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-gradient-to-r from-transparent via-sky-500 to-transparent" />
|
||||
<div className="relative z-30 text-base font-bold text-white">{item.name}</div>
|
||||
<div className="text-xs text-white">{item.designation}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<img
|
||||
onMouseMove={handleMouseMove}
|
||||
height={100}
|
||||
width={100}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="relative !m-0 h-14 w-14 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cn } from "cn";
|
||||
|
||||
function AspectRatio({
|
||||
ratio,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { ratio: number }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="aspect-ratio"
|
||||
style={
|
||||
{
|
||||
"--ratio": ratio,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn("relative aspect-(--ratio)", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -0,0 +1,195 @@
|
||||
import * as React from "react";
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "cn";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const attachmentVariants = cva(
|
||||
"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default:
|
||||
"gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2",
|
||||
sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",
|
||||
xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
|
||||
},
|
||||
orientation: {
|
||||
horizontal: "min-w-40 items-center",
|
||||
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Attachment({
|
||||
className,
|
||||
state = "done",
|
||||
size = "default",
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof attachmentVariants> & {
|
||||
state?: "idle" | "uploading" | "processing" | "error" | "done";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment"
|
||||
data-state={state}
|
||||
data-size={size}
|
||||
data-orientation={orientation}
|
||||
className={cn(attachmentVariants({ size, orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const attachmentMediaVariants = cva(
|
||||
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-noLine truncated
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
icon: "",
|
||||
image:
|
||||
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "icon",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function AttachmentMedia({
|
||||
className,
|
||||
variant = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-media"
|
||||
data-variant={variant}
|
||||
className={cn(attachmentMediaVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-content"
|
||||
className={cn(
|
||||
"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="attachment-title"
|
||||
className={cn(
|
||||
"block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="attachment-description"
|
||||
className={cn(
|
||||
"mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",
|
||||
"max-w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-actions"
|
||||
className={cn(
|
||||
"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentAction({
|
||||
className,
|
||||
variant,
|
||||
size = "icon-xs",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
return (
|
||||
<Button
|
||||
data-slot="attachment-action"
|
||||
variant={variant ?? "ghost"}
|
||||
size={size}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentTrigger({
|
||||
className,
|
||||
render,
|
||||
type,
|
||||
...props
|
||||
}: useRender.ComponentProps<"button">) {
|
||||
return useRender({
|
||||
defaultTagName: "button",
|
||||
props: mergeProps<"button">(
|
||||
{
|
||||
type: render ? type : (type ?? "button"),
|
||||
className: cn("absolute inset-0 z-10 outline-none", className),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "attachment-trigger",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-group"
|
||||
className={cn(
|
||||
"flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Attachment,
|
||||
AttachmentGroup,
|
||||
AttachmentMedia,
|
||||
AttachmentContent,
|
||||
AttachmentTitle,
|
||||
AttachmentDescription,
|
||||
AttachmentActions,
|
||||
AttachmentAction,
|
||||
AttachmentTrigger,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-bg relative flex h-[100vh] flex-col items-center justify-center bg-zinc-50 text-slate-950 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={
|
||||
{
|
||||
"--aurora":
|
||||
"repeating-linear-gradient(100deg,#3b82f6_10%,#a5b4fc_15%,#93c5fd_20%,#ddd6fe_25%,#60a5fa_30%)",
|
||||
"--dark-gradient":
|
||||
"repeating-linear-gradient(100deg,#000_0%,#000_7%,transparent_10%,transparent_12%,#000_16%)",
|
||||
"--white-gradient":
|
||||
"repeating-linear-gradient(100deg,#fff_0%,#fff_7%,transparent_10%,transparent_12%,#fff_16%)",
|
||||
|
||||
"--blue-300": "#93c5fd",
|
||||
"--blue-400": "#60a5fa",
|
||||
"--blue-500": "#3b82f6",
|
||||
"--indigo-300": "#a5b4fc",
|
||||
"--violet-200": "#ddd6fe",
|
||||
"--black": "#000",
|
||||
"--white": "#fff",
|
||||
"--transparent": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`after:animate-aurora pointer-events-none absolute -inset-[10px] [background-image:var(--white-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] opacity-50 blur-[10px] invert filter will-change-transform [--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)] [--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--trLine truncated
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
||||
import { cn } from "cn";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg";
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full rounded-full object-cover", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BackgroundBeams = React.memo(({ className }: { className?: string }) => {
|
||||
const paths = [
|
||||
"M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875",
|
||||
"M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867",
|
||||
"M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859",
|
||||
"M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851",
|
||||
"M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843",
|
||||
"M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835",
|
||||
"M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827",
|
||||
"M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819",
|
||||
"M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811",
|
||||
"M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803",
|
||||
"M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795",
|
||||
"M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787",
|
||||
"M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779",
|
||||
"M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771",
|
||||
"M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763",
|
||||
"M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755",
|
||||
"M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747",
|
||||
"M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739",
|
||||
"M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731",
|
||||
"M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723",
|
||||
"M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715",
|
||||
"M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707",
|
||||
"M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699",
|
||||
"M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691",
|
||||
"M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683",
|
||||
"M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675",
|
||||
"M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667",
|
||||
"M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659",
|
||||
"M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651",
|
||||
"M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643",
|
||||
"M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635",
|
||||
"M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627",
|
||||
"M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619",
|
||||
"M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611",
|
||||
"M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603",
|
||||
"M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595",
|
||||
"M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587",
|
||||
"M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579",
|
||||
"M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571",
|
||||
"M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563",
|
||||
"M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555",
|
||||
"M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547",
|
||||
"M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539",
|
||||
"M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531",
|
||||
"M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523",
|
||||
"M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515",
|
||||
"M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507",
|
||||
"M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499",
|
||||
"M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491",
|
||||
"M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex h-full w-full items-center justify-center [mask-size:40px] [mask-repeat:no-repeat]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="pointer-events-none absolute z-0 h-full w-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 696 316"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 8Line truncated
|
||||
stroke="url(#paint0_radial_242_278)"
|
||||
strokeOpacity="0.05"
|
||||
strokeWidth="0.5"
|
||||
></path>
|
||||
|
||||
{paths.map((path, index) => (
|
||||
<motion.path
|
||||
key={`path-` + index}
|
||||
d={path}
|
||||
stroke={`url(#linearGradient-${index})`}
|
||||
strokeOpacity="0.4"
|
||||
strokeWidth="0.5"
|
||||
></motion.path>
|
||||
))}
|
||||
<defs>
|
||||
{paths.map((_path, index) => (
|
||||
<motion.linearGradient
|
||||
id={`linearGradient-${index}`}
|
||||
key={`gradient-${index}`}
|
||||
initial={{
|
||||
x1: "0%",
|
||||
x2: "0%",
|
||||
y1: "0%",
|
||||
y2: "0%",
|
||||
}}
|
||||
animate={{
|
||||
x1: ["0%", "100%"],
|
||||
x2: ["0%", "95%"],
|
||||
y1: ["0%", "100%"],
|
||||
y2: ["0%", `${93 + Math.random() * 8}%`],
|
||||
}}
|
||||
transition={{
|
||||
duration: Math.random() * 10 + 10,
|
||||
ease: "easeInOut",
|
||||
repeat: Infinity,
|
||||
delay: Math.random() * 10,
|
||||
}}
|
||||
>
|
||||
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
|
||||
<stop stopColor="#18CCFC"></stop>
|
||||
<stop offset="32.5%" stopColor="#6344F5"></stop>
|
||||
<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
|
||||
</motion.linearGradient>
|
||||
))}
|
||||
|
||||
<radialGradient
|
||||
id="paint0_radial_242_278"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
|
||||
>
|
||||
<stop offset="0.0666667" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.243243" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.43594" stopColor="white" stopOpacity="0"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
BackgroundBeams.displayName = "BackgroundBeams";
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BoxesCore = ({ className, ...rest }: { className?: string }) => {
|
||||
const rows = new Array(150).fill(1);
|
||||
const cols = new Array(100).fill(1);
|
||||
const colors = [
|
||||
"#93c5fd",
|
||||
"#f9a8d4",
|
||||
"#86efac",
|
||||
"#fde047",
|
||||
"#fca5a5",
|
||||
"#d8b4fe",
|
||||
"#93c5fd",
|
||||
"#a5b4fc",
|
||||
"#c4b5fd",
|
||||
];
|
||||
const getRandomColor = () => {
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`,
|
||||
}}
|
||||
className={cn(
|
||||
"absolute -top-1/4 left-1/4 z-0 flex h-full w-full -translate-x-1/2 -translate-y-1/2 p-4",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{rows.map((_, i) => (
|
||||
<motion.div key={`row` + i} className="relative h-8 w-16 border-l border-slate-700">
|
||||
{cols.map((_, j) => (
|
||||
<motion.div
|
||||
whileHover={{
|
||||
backgroundColor: `${getRandomColor()}`,
|
||||
transition: { duration: 0 },
|
||||
}}
|
||||
animate={{
|
||||
transition: { duration: 2 },
|
||||
}}
|
||||
key={`col` + j}
|
||||
className="relative h-8 w-16 border-t border-r border-slate-700"
|
||||
>
|
||||
{j % 2 === 0 && i % 2 === 0 ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className="pointer-events-none absolute -top-[14px] -left-[22px] h-6 w-10 stroke-[1px] text-slate-700"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
) : null}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Boxes = React.memo(BoxesCore);
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const BackgroundGradientAnimation = ({
|
||||
gradientBackgroundStart = "rgb(108, 0, 162)",
|
||||
gradientBackgroundEnd = "rgb(0, 17, 82)",
|
||||
firstColor = "18, 113, 255",
|
||||
secondColor = "221, 74, 255",
|
||||
thirdColor = "100, 220, 255",
|
||||
fourthColor = "200, 50, 50",
|
||||
fifthColor = "180, 180, 50",
|
||||
pointerColor = "140, 100, 255",
|
||||
size = "80%",
|
||||
blendingValue = "hard-light",
|
||||
children,
|
||||
className,
|
||||
interactive = true,
|
||||
containerClassName,
|
||||
}: {
|
||||
gradientBackgroundStart?: string;
|
||||
gradientBackgroundEnd?: string;
|
||||
firstColor?: string;
|
||||
secondColor?: string;
|
||||
thirdColor?: string;
|
||||
fourthColor?: string;
|
||||
fifthColor?: string;
|
||||
pointerColor?: string;
|
||||
size?: string;
|
||||
blendingValue?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
interactive?: boolean;
|
||||
containerClassName?: string;
|
||||
}) => {
|
||||
const interactiveRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [curX, setCurX] = useState(0);
|
||||
const [curY, setCurY] = useState(0);
|
||||
const [tgX, setTgX] = useState(0);
|
||||
const [tgY, setTgY] = useState(0);
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty("--gradient-background-start", gradientBackgroundStart);
|
||||
document.body.style.setProperty("--gradient-background-end", gradientBackgroundEnd);
|
||||
document.body.style.setProperty("--first-color", firstColor);
|
||||
document.body.style.setProperty("--second-color", secondColor);
|
||||
document.body.style.setProperty("--third-color", thirdColor);
|
||||
document.body.style.setProperty("--fourth-color", fourthColor);
|
||||
document.body.style.setProperty("--fifth-color", fifthColor);
|
||||
document.body.style.setProperty("--pointer-color", pointerColor);
|
||||
document.body.style.setProperty("--size", size);
|
||||
document.body.style.setProperty("--blending-value", blendingValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function move() {
|
||||
if (!interactiveRef.current) {
|
||||
return;
|
||||
}
|
||||
setCurX(curX + (tgX - curX) / 20);
|
||||
setCurY(curY + (tgY - curY) / 20);
|
||||
interactiveRef.current.style.transform = `translate(${Math.round(
|
||||
curX
|
||||
)}px, ${Math.round(curY)}px)`;
|
||||
}
|
||||
|
||||
move();
|
||||
}, [tgX, tgY]);
|
||||
|
||||
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (interactiveRef.current) {
|
||||
const rect = interactiveRef.current.getBoundingClientRect();
|
||||
setTgX(event.clientX - rect.left);
|
||||
setTgY(event.clientY - rect.top);
|
||||
}
|
||||
};
|
||||
|
||||
const [isSafari, setIsSafari] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative top-0 left-0 h-screen w-screen overflow-hidden bg-[linear-gradient(40deg,var(--gradient-background-start),var(--gradient-background-end))]",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<svg className="hidden">
|
||||
<defs>
|
||||
<filter id="blurMe">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
mode="matrix"
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8"
|
||||
result="goo"
|
||||
/>
|
||||
<feBlend in="SourceGraphic" in2="goo" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className={cn("", className)}>{children}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"gradients-container h-full w-full blur-lg",
|
||||
isSafari ? "blur-2xl" : "[filter:url(#blurMe)_blur(40px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_var(--first-color)_0,_var(--first-color)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:center_center]`,
|
||||
`animate-first`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--second-color),_0.8)_0,_rgba(var(--second-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-400px)]`,
|
||||
`animate-second`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--third-color),_0.8)_0,_rgba(var(--third-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%+400px)]`,
|
||||
`animate-third`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fourth-color),_0.8)_0,_rgba(var(--fourth-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-200px)]`,
|
||||
`animate-fourth`,
|
||||
`opacity-70`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fifth-color),_0.8)_0,_rgba(var(--fifth-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-800px)_calc(50%+800px)]`,
|
||||
`animate-fifth`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
|
||||
{interactive && (
|
||||
<div
|
||||
ref={interactiveRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--pointer-color),_0.8)_0,_rgba(var(--pointer-color),_0)_50%)_no-repeat]`,
|
||||
`-top-1/2 -left-1/2 h-full w-full [mix-blend-mode:var(--blending-value)]`,
|
||||
`opacity-70`
|
||||
)}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loaded 50 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user