Compare commits

...

8 Commits

Author SHA1 Message Date
7a27b3a5ae Update IDE config and seed file with additional data
Adjusted .idea/workspace.xml for correct recent project path and tasks. Updated seed.ts to include new cryptocurrency records and modified existing promo code details.
2024-11-27 10:55:28 +01:00
ccbb1d749a Some ide config files and libs changes... 2024-11-26 19:59:12 +01:00
b6f50dc944 Add defaultCryptoId prop to SellForm and enhance usability
Introduced a new defaultCryptoId prop for the SellForm component to pre-select a cryptocurrency on mount. Adjusted responsive layout and usability for various components including headers, modals, and navigation links. Ensured better user feedback and message clarity for login and registration processes.
2024-11-24 23:50:12 +01:00
e3aa219389 Remove IDE configuration files and add new selling feature
Removed unnecessary IntelliJ IDEA (`.idea`) configuration files to clean up the repository. Added new sell-related functionality including the SellCryptoPage, SellModal, SellForm, and updated relevant types and UI components.
2024-11-22 11:06:05 +01:00
ea7ab60e5c Add QueryClientProvider for react-query support
Integrate react-query by adding `QueryClientProvider` to the application. This involves updating dependencies, wrapping existing providers with `QueryProvider`, and refactoring the `account-info` module to utilize new hooks and APIs.
2024-11-14 16:04:14 +01:00
a75a87f683 Add initial Neptune Frontend setup
Initial setup of the Neptune frontend project, including Dockerfile, environment files, TypeScript configuration, and essential components. Added basic page structures for dashboard and wallet, and configured Tailwind CSS and postcss.
2024-11-14 11:10:32 +01:00
442bebd022 Add various UI components including ToggleGroup and RadioGroup
This commit introduces multiple new UI components for the project. The added components include ToggleGroup, RadioGroup, Pagination, ToastBox, Textarea, Breadcrumb, Skeleton, Collapsible, Checkbox, Calendar, Accordion, Sonner, and CopyButton. These components enhance the flexibility and functionality of the UI, providing essential elements for better user interaction.
2024-11-08 12:20:27 +01:00
a1c12f09f6 Add AutoForm components and utilities
Introduce core components and utilities for AutoForm, including form configuration, field types, dependency resolution, and schema handling using Zod. These additions provide a structured and dynamic form generation system with dependency management and validation integration.
2024-11-08 12:20:14 +01:00
135 changed files with 14152 additions and 1 deletions

1
.env.development.example Normal file
View File

@@ -0,0 +1 @@
NEXT_PUBLIC_API_BASE_URL: ""

1
.env.production.example Normal file
View File

@@ -0,0 +1 @@
NEXT_PUBLIC_API_BASE_URL: "localhost:3333"

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env.production
.env.development
.env*.local
/.env

7
.idea/discord.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT" />
<option name="description" value="" />
</component>
</project>

6
.idea/git_toolbox_blame.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

9
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/../neptune-back/.idea/neptune-backend.iml" filepath="$PROJECT_DIR$/../neptune-back/.idea/neptune-backend.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/neptune-front.iml" filepath="$PROJECT_DIR$/.idea/neptune-front.iml" />
</modules>
</component>
</project>

13
.idea/neptune-front.iml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="neptune-backend" />
</component>
</module>

6
.idea/nx-console.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NxConsoleProjectSettingsProvider">
<option name="workspacePath" value="" />
</component>
</project>

7
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../neptune-back" vcs="Git" />
</component>
</project>

21
.idea/webResources.xml generated Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$/../neptune-back">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/../neptune-back" />
</resourceRoots>
</entryData>
</entry>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

155
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="fe5a67da-16c2-44c1-97cf-5ff76c380f48" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/../neptune-back/prisma/seed.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../neptune-back/prisma/seed.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../neptune-back" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="2oC9EyMrMXnN5JRY3d8EUK3Fg2I" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/avnyr/monoliths/neptune-back&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;pnpm&quot;,
&quot;npm.dev.executor&quot;: &quot;Run&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;,
&quot;ts.external.directory.path&quot;: &quot;/home/avnyr/monoliths/neptune-front/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/public" />
</key>
</component>
<component name="RunManager">
<configuration name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<package-manager value="$USER_HOME$/.local/share/pnpm/pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-e768b9ed790e-JavaScript-WS-243.21565.180" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="fe5a67da-16c2-44c1-97cf-5ff76c380f48" name="Changes" comment="" />
<created>1730362584431</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1730362584431</updated>
<workItem from="1730362585545" duration="2622000" />
<workItem from="1730365668445" duration="599000" />
<workItem from="1730370820744" duration="1688000" />
<workItem from="1730715257858" duration="38000" />
<workItem from="1730971311881" duration="4339000" />
<workItem from="1732021386744" duration="27000" />
<workItem from="1732021424352" duration="4000" />
<workItem from="1732028162494" duration="139000" />
<workItem from="1732110655402" duration="2406000" />
<workItem from="1732115490707" duration="1304000" />
<workItem from="1732177377647" duration="212000" />
<workItem from="1732177642993" duration="25000" />
<workItem from="1732177670017" duration="26000" />
<workItem from="1732177712103" duration="20000" />
<workItem from="1732177736286" duration="34000" />
<workItem from="1732202348717" duration="1808000" />
<workItem from="1732268531361" duration="8833000" />
<workItem from="1732453974909" duration="11758000" />
<workItem from="1732526630314" duration="3449000" />
<workItem from="1732646974158" duration="1446000" />
<workItem from="1732696522710" duration="884000" />
</task>
<task id="LOCAL-00001" summary="Remove IDE configuration files and add new selling feature&#10;&#10;Removed unnecessary IntelliJ IDEA (`.idea`) configuration files to clean up the repository. Added new sell-related functionality including the SellCryptoPage, SellModal, SellForm, and updated relevant types and UI components.">
<option name="closed" value="true" />
<created>1732269965774</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1732269965774</updated>
</task>
<task id="LOCAL-00002" summary="Enable user crypto amount selection and update deployment docs&#10;&#10;Added the ability to select user crypto amounts in the user service. Removed commented-out code in the offer controller for clarity. Updated the README with deployment and production instructions.">
<option name="closed" value="true" />
<created>1732270010458</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1732270010458</updated>
</task>
<task id="LOCAL-00003" summary="Fix offer retrieval method and add GDPR acknowledgment&#10; &#10;Return the result from `getOffersById` method to fix route handling. Add `id` field in offers selection query and introduce `gdpr_acknowledgement` to the user model and mock data for GDPR compliance.">
<option name="closed" value="true" />
<created>1732488599143</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1732488599143</updated>
</task>
<task id="LOCAL-00004" summary="Add defaultCryptoId prop to SellForm and enhance usability&#10;&#10;Introduced a new defaultCryptoId prop for the SellForm component to pre-select a cryptocurrency on mount. Adjusted responsive layout and usability for various components including headers, modals, and navigation links. Ensured better user feedback and message clarity for login and registration processes.">
<option name="closed" value="true" />
<created>1732488612776</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1732488612776</updated>
</task>
<task id="LOCAL-00005" summary="Some ide config files and libs changes...">
<option name="closed" value="true" />
<created>1732647552711</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1732647552711</updated>
</task>
<option name="localTasksCounter" value="6" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" />
<MESSAGE value="Remove IDE configuration files and add new selling feature&#10;&#10;Removed unnecessary IntelliJ IDEA (`.idea`) configuration files to clean up the repository. Added new sell-related functionality including the SellCryptoPage, SellModal, SellForm, and updated relevant types and UI components." />
<MESSAGE value="Enable user crypto amount selection and update deployment docs&#10;&#10;Added the ability to select user crypto amounts in the user service. Removed commented-out code in the offer controller for clarity. Updated the README with deployment and production instructions." />
<MESSAGE value="Fix offer retrieval method and add GDPR acknowledgment&#10; &#10;Return the result from `getOffersById` method to fix route handling. Add `id` field in offers selection query and introduce `gdpr_acknowledgement` to the user model and mock data for GDPR compliance." />
<MESSAGE value="Add defaultCryptoId prop to SellForm and enhance usability&#10;&#10;Introduced a new defaultCryptoId prop for the SellForm component to pre-select a cryptocurrency on mount. Adjusted responsive layout and usability for various components including headers, modals, and navigation links. Ensured better user feedback and message clarity for login and registration processes." />
<MESSAGE value="Some ide config files and libs changes..." />
<option name="LAST_COMMIT_MESSAGE" value="Some ide config files and libs changes..." />
</component>
</project>

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# Étape 1 : Build
FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm
# Copier package.json et installer les dépendances
COPY pnpm-lock.yaml ./
COPY package.json ./
RUN pnpm install
# Copier le reste du code et construire l'application
COPY . .
RUN pnpm run build
# Étape 2 : Serveur de production
FROM node:18-alpine
WORKDIR /app
# Copier uniquement les fichiers nécessaires
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
# Exposer le port de l'application
EXPOSE 3000
# Commande de démarrage
CMD ["npm", "start"]

View File

@@ -1,3 +1,16 @@
# neptune-front
The project to validate my DWWM diploma on the front end.
## Déploiement
- Configurer les variables d'environnements
### En développement
```shell
pnpm dev
```
### En production
```shell
pnpm build
pnpm start
```

37
biome.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
},
"vcs": {
"enabled": true,
"clientKind": "git"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"performance": {
"recommended": true,
"noDelete": "off"
},
"suspicious": {
"noExplicitAny": "warn"
},
"complexity": {
"useLiteralKeys": "off"
}
}
},
"formatter": {
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 90
}
}

17
components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

4
next.config.mjs Normal file
View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

80
package.json Normal file
View File

@@ -0,0 +1,80 @@
{
"name": "neptune-back",
"version": "0.1.0",
"author": "Mathis HERRIOT",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
"check": "biome check --skip-errors --write src"
},
"dependencies": {
"@fontsource-variable/kode-mono": "^5.1.0",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.60.2",
"@tanstack/react-table": "^8.20.5",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.1",
"framer-motion": "^11.11.15",
"input-otp": "^1.4.1",
"lightweight-charts": "^4.2.1",
"lucide-react": "^0.395.0",
"next": "14.2.18",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-resizable-panels": "^2.1.6",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "1.8.1",
"@types/jest": "^29.5.14",
"@types/node": "^20.17.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"jest": "^29.7.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
}
}

5382
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/neptune.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

54
public/neptune.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

10
src/app/auth/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { AuthForms } from "@/components/auth-form";
import Image from "next/image";
export default function AuthPage() {
return (
<main className="flex flex-col items-center justify-start h-full w-full">
<AuthForms />
</main>
);
}

View File

@@ -0,0 +1,9 @@
export default function AdminPage() {
return (
<>
<section>
<h1>Welcome to the Dashboard</h1>
</section>
</>
);
}

View File

@@ -0,0 +1,58 @@
"use client";
import { CryptosTable } from "@/components/data-tables/cryptos-table";
import { Skeleton } from "@/components/ui/skeleton";
import type { IApiAllOffersRes } from "@/interfaces/api.interface";
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
import ApiRequest from "@/services/apiRequest";
import { useEffect, useState } from "react";
export default function DashboardPage() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [cryptosList, setCryptosList] = useState<ICryptoInWalletInfo[]>([]);
useEffect(() => {
ApiRequest.authenticated.get
.json<ICryptoInWalletInfo[]>("crypto/all")
.then((response) => {
if (response.data.length <= 0) {
setCryptosList([]);
setIsLoading(false);
return;
}
const resp = response.data;
setCryptosList(resp);
setIsLoading(false);
});
}, []);
if (isLoading) {
return (
<div
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
}
>
<h1 className={"text-2xl font-bold"}>Available cryptos</h1>
<Skeleton
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
}
/>
</div>
);
}
return (
<>
<section
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
}
>
<h1 className={"text-2xl font-bold"}>Available cryptos</h1>
<CryptosTable cryptosArray={cryptosList} />
</section>
</>
);
}

65
src/app/globals.css Normal file
View File

@@ -0,0 +1,65 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Kode Mono Variable', monospace;
}
@layer base {
:root {
--background: 289 20.6% 90.87%;
--foreground: 289 71% 3%;
--muted: 291.15 0% 100%;
--muted-foreground: 109 0% 39%;
--popover: 289 62% 97%;
--popover-foreground: 289 71% 2%;
--card: 286.67 60% 97.06%;
--card-foreground: 289 71% 2%;
--border: 289 0% 78.21%;
--input: 289 14.91% 46.64%;
--primary: 271.89 62.45% 55.1%;
--primary-foreground: 289 39% 96%;
--secondary: 271.83 81.56% 27.65%;
--secondary-foreground: 0 0% 100%;
--accent: 36.14 100% 67.45%;
--accent-foreground: 0 0% 12.94%;
--destructive: 8 89% 28%;
--destructive-foreground: 8 89% 88%;
--ring: 289 39% 36%;
--radius: 0.5rem;
}
.dark {
--background: 289 47.53% 5.21%;
--foreground: 289 6.69% 90.11%;
--muted: 271.83 26.29% 22.62%;
--muted-foreground: 0 0% 73.85%;
--popover: 289 17.57% 17.45%;
--popover-foreground: 0 0% 89.23%;
--card: 289 50.44% 6.99%;
--card-foreground: 300 5.88% 90%;
--border: 289 5.38% 31.15%;
--input: 289 12.37% 29.89%;
--primary: 271.89 62.45% 55.1%;
--primary-foreground: 289 39% 96%;
--secondary: 271.83 81.56% 27.65%;
--secondary-foreground: 300 5.88% 90%;
--accent: 36.14 100% 67.45%;
--accent-foreground: 0 0% 12.94%;
--destructive: 350.05 93.43% 41.76%;
--destructive-foreground: 0 0% 92.55%;
--ring: 289 39% 36%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

46
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,46 @@
import type { Metadata } from "next";
import "@fontsource-variable/kode-mono";
import "./globals.css";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { PrimaryNavigationMenu } from "@/components/primary-nav";
import { Providers } from "@/components/providers/providers";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/toaster";
import Link from "next/link";
import type React from "react";
export const metadata: Metadata = {
title: "Neptune Crypto",
description: "A fictive app",
icons: "/neptune.svg",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={"w-full min-h-screen flex flex-col items-center justify-between"}>
<Providers>
<Header>
<nav className="flex flex-row flex-wrap md:flex-nowrap gap-2">
<Button asChild variant="light">
<Link href="/wallet" title={"Go to your wallet"}><p className="lg:text-lg">Wallet</p></Link>
</Button>
<Button asChild variant="light">
<Link href="/dashboard" title={"Go to the crypto explorer"}><p className="lg:text-lg">Explore cryptos</p></Link>
</Button>
</nav>
</Header>
{children}
<Toaster/>
<Footer/>
</Providers>
</body>
</html>
);
}

89
src/app/page.tsx Normal file
View File

@@ -0,0 +1,89 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Bitcoin, DollarSign, LineChart, Lock, Zap } from "lucide-react";
import Link from "next/link";
export default function HomePage() {
return (
<div className="flex flex-col min-h-screen">
<main className="flex-1">
<section className="w-full py-12 md:py-16 lg:py-24 xl:py-32">
<div className="container px-4 md:px-6">
<div className="flex flex-col items-center space-y-4 text-center">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl">
Welcome to Neptune Crypto
</h1>
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
Your gateway to the world of cryptocurrencies. Trade, invest, and grow your digital assets with ease.
</p>
</div>
<div className="space-x-4">
<Button>Get Started</Button>
<Button variant="outline">Learn More</Button>
</div>
<p className="mx-auto max-w-[700px] text-destructive/50 md:text-lg">
This website is fictional and for demonstration purposes only.
</p>
</div>
</div>
</section>
<section className="w-full py-12 md:py-16 lg:py-20 bg-card rounded">
<div className="container px-4 md:px-6 rounded">
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl text-center mb-12">Our Features</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<Bitcoin className="h-10 w-10 mb-2"/>
<CardTitle>Multiple Cryptocurrencies</CardTitle>
<CardDescription>Trade a wide variety of popular cryptocurrencies.</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<Lock className="h-10 w-10 mb-2"/>
<CardTitle>Secure Storage</CardTitle>
<CardDescription>Your assets are protected with state-of-the-art security measures.</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<LineChart className="h-10 w-10 mb-2"/>
<CardTitle>Advanced Trading Tools</CardTitle>
<CardDescription>Access powerful analytics and trading features.</CardDescription>
</CardHeader>
</Card>
</div>
</div>
</section>
<section className="w-full py-12 md:py-24 lg:py-32">
<div className="container px-4 md:px-6">
<div className="flex flex-col items-center justify-center space-y-4 text-center">
<div className="space-y-2">
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">Start Trading Today</h2>
<p className="mx-auto max-w-[600px] text-gray-500 md:text-xl lg:text-base xl:text-xl dark:text-gray-400">
Join thousands of traders and investors on our platform. Get started with as little as $10.
</p>
</div>
<div className="w-full max-w-sm space-y-2">
<Button>
Sign Up
<DollarSign className="ml-2 h-4 w-4"/>
</Button>
<p className="text-xs text-gray-500 dark:text-gray-400">
By signing up, you agree to our{" "}
<Link title="Go to legal notice" className="underline underline-offset-2" href="/legal">
Legal Notice
</Link>
</p>
</div>
</div>
</div>
</section>
</main>
</div>
);
}

View File

@@ -0,0 +1,24 @@
"use client"
import {OfferList} from "@/components/sub/OfferList";
import {SellForm} from "@/components/sell-form";
interface SellCryptoPageProps {
params: IParams;
}
interface IParams {
cryptoId: string;
}
export default function SellCryptoPage({ params }: SellCryptoPageProps) {
console.log(params);
return (
<main className="flex flex-col sm:flex-row items-center justify-center w-full h-full gap-4 p-2 sm:p-4">
<section className={"w-full lg:w-1/3 h-full p-0.5 sm:p-2 flex flex-col items-center justify-center gap-4"}>
<SellForm defaultCryptoId={params.cryptoId}/>
</section>
</main>
);
}

56
src/app/wallet/page.tsx Normal file
View File

@@ -0,0 +1,56 @@
"use client";
import { CryptosTable } from "@/components/data-tables/cryptos-table";
import { WalletTable } from "@/components/data-tables/wallet-table";
import { UserDataContext } from "@/components/providers/userdata-provider";
import { Skeleton } from "@/components/ui/skeleton";
import type { IApiAllOffersRes } from "@/interfaces/api.interface";
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
import type { IUserWallet } from "@/interfaces/userdata.interface";
import ApiRequest from "@/services/apiRequest";
import { useContext, useEffect, useState } from "react";
export default function WalletPage() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [cryptosList, setCryptosList] = useState<ICryptoInWalletInfo[]>([]);
const userContext = useContext(UserDataContext);
useEffect(() => {
console.log(userContext?.userData);
if (userContext?.userData) {
setIsLoading(false);
}
}, [userContext]);
if (isLoading || !userContext?.userData) {
return (
<div
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
}
>
<h1 className={"text-2xl font-bold"}>Loading in progress ...</h1>
<Skeleton
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
}
/>
</div>
);
}
return (
<>
<section
className={
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4 rounded sm:p-2 sm:bg-card"
}
>
<h1 className={"text-xl sm:text-2xl lg:text-3xl font-bold"}>Cryptos in your wallet</h1>
<WalletTable
walletArray={userContext.userData.wallet as unknown as IUserWallet}
/>
</section>
</>
);
}

View File

@@ -0,0 +1,53 @@
"use client";
import { AccountInfo } from "@/components/account-info";
import { UserDataContext } from "@/components/providers/userdata-provider";
import { Skeleton } from "@/components/ui/skeleton";
import type { IUserData } from "@/interfaces/userdata.interface";
import {
type Dispatch,
type SetStateAction,
useContext,
useEffect,
useState,
} from "react";
const localStorage = typeof window !== "undefined" ? window.localStorage : null;
export function AccountDialog() {
const userContext = useContext(UserDataContext);
const token = localStorage?.getItem("sub") || "";
const haveToken = token.length >= 16 || false;
console.log(haveToken);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
if (!userContext) {
return (
<div>
<p>No account</p>
</div>
);
}
useEffect(() => {
if (userContext?.userData) {
setIsLoaded(true);
}
}, [userContext?.userData]);
if (!isLoaded) {
return <Skeleton className="w-14 h-10 rounded" />;
}
return (
<div>
<AccountInfo
userData={userContext?.userData as IUserData}
setUserData={
userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>
}
isDisconnected={!haveToken}
/>
</div>
);
}

View File

@@ -0,0 +1,182 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { IUserData, IUserWallet } from "@/interfaces/userdata.interface";
import { CopyButton } from "@/components/ui/copy-button";
import type {
ICryptoInUserWalletInfo,
} from "@/interfaces/crypto.interface";
import { Bitcoin, Fingerprint, Key, Landmark, Unplug, User, Wallet } from "lucide-react";
import Link from "next/link";
import type React from "react";
import { useEffect, useState, useCallback, useMemo } from "react";
import { EReturnState, type IStandardisedReturn } from "@/interfaces/general.interface";
import type { IApiUserAssetsRes } from "@/interfaces/api.interface";
import ApiRequest from "@/services/apiRequest";
export function doDisconnect() {
if (typeof window !== "undefined") {
window.localStorage.removeItem("sub");
//Redirect to homepage
window.location.href = "/";
return true;
}
console.log("Whut ? Why trying to remove an item from the localStorage when running in SSR ?");
return false;
}
async function getWallet(): Promise<IStandardisedReturn<IApiUserAssetsRes>> {
try {
const ReqRes =
await ApiRequest.authenticated.get.json<IStandardisedReturn<IApiUserAssetsRes>>(
"user/my-assets"
);
console.log(ReqRes.data);
if (ReqRes.status !== 200) {
return {
state: EReturnState.clientError,
};
}
return {
state: EReturnState.done,
resolved: ReqRes.data,
};
} catch (err) {
return {
state: EReturnState.serverError,
};
}
}
function useWallet(userData: IUserData, setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>) {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
if (!isLoaded) {
getWallet().then((res) => {
const wallet: IUserWallet = {
uat: Date.now(),
update_interval: 30000,
owned_cryptos: res.resolved?.UserHasCrypto?.map((el): ICryptoInUserWalletInfo => ({
id: el.Crypto.id,
name: el.Crypto.name,
value: el.Crypto.value,
image: el.Crypto.image,
quantity: el.Crypto.quantity,
owned_amount: el.amount,
created_at: el.Crypto.created_at,
updated_at: el.Crypto.updated_at,
})) || [],
};
delete res.resolved?.UserHasCrypto;
setUserData((prev) => ({
...prev,
...res.resolved,
wallet,
}) as unknown as IUserData);
console.log(userData);
setIsLoaded(true);
});
}
}, [isLoaded, userData, setUserData]);
return isLoaded;
}
export function AccountInfo({
userData,
setUserData,
isDisconnected,
}: {
userData: IUserData;
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
isDisconnected: boolean;
}) {
const isLoaded = useWallet(userData, setUserData);
const walletInfo = useMemo(() => (
<div className={"flex flex-col md:flex-row gap-2 justify-center md:justify-evenly items-start md:items-center w-full"}>
<div className={"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"}>
<Landmark />
<p className={"rounded bg-accent text-accent-foreground p-1"}>{userData.dollarAvailables} $</p>
</div>
<div className={"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"}>
<Bitcoin />
<p className={"rounded bg-accent text-accent-foreground p-1"}>{`You currently have ${userData.wallet.owned_cryptos.length} crypto(s)`}</p>
</div>
</div>
), [userData.dollarAvailables, userData.wallet.owned_cryptos.length]);
const userInfo = useMemo(() => (
<div className={"flex flex-col gap-3 justify-center items-start mx-auto mt-4"}>
<div className={"flex flex-row text-nowrap flex-nowrap gap-1 text-primary"}>
<Fingerprint />
<h2>Your identity</h2>
</div>
<div className={"font-light text-xs md:text-sm flex flex-row items-center justify-start gap-1 bg-accent p-2 rounded"}>
<p>{userData.id}</p>
<CopyButton value={userData.id} />
</div>
</div>
), [userData.id]);
if (isDisconnected) {
return (
<div className={"flex flex-col justify-center items-center h-10 p-2 text-xs mt-2"}>
<div className={"flex flex-row justify-center items-center gap-1 text-destructive to-red-900 animate-pulse"}>
<Unplug className={"w-4"} />
<p>Disconnected</p>
</div>
<div>
<Link href={"/auth"} className={"hover:text-primary flex justify-evenly items-center gap-1 p-1 text-nowrap"}>
<Key className={"w-3"} /> Link account
</Link>
</div>
</div>
);
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className={"gap-2 px-2"}>
<p>{userData.firstName}</p>
<User />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[720px]">
<DialogHeader>
<DialogTitle>{`Your account - ${userData.firstName} ${userData.lastName}`}</DialogTitle>
<DialogDescription>{userData.pseudo}</DialogDescription>
</DialogHeader>
<div className={"flex flex-col items-center justify-center w-full"}>
{walletInfo}
{userInfo}
</div>
<DialogFooter>
<Button variant={"secondary"} className={"gap-2 px-2"} asChild>
<Link href={"/wallet"}>
<Wallet />
<p>My wallet</p>
</Link>
</Button>
<Button variant={"destructive"} className={"gap-2 px-2 mb-2"} onClick={() => doDisconnect()}>
<Unplug />
<p>Disconnect</p>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,262 @@
"use client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import AutoForm, { AutoFormSubmit } from "@/components/auto-form";
import { UserDataContext } from "@/components/providers/userdata-provider";
import { ToastBox, toastType } from "@/components/ui/toast-box";
import { useToast } from "@/components/ui/use-toast";
import type {
IApiLoginReq,
IApiLoginRes,
IApiRegisterReq,
IApiRegisterRes,
} from "@/interfaces/api.interface";
import { EReturnState, type IStandardisedReturn } from "@/interfaces/general.interface";
import type { IUserData } from "@/interfaces/userdata.interface";
import ApiRequest from "@/services/apiRequest";
import { useLocalStorage } from "@/services/localStorage";
import { Bug, RefreshCw } from "lucide-react";
import Link from "next/link";
import React, { type Dispatch, type SetStateAction, useContext, useState } from "react";
import * as z from "zod";
import Image from "next/image";
const loginSchema = z.object({
email: z
.string({
required_error: "Email is needed.",
})
.email({
message: "Should be a valid email.",
})
.describe("Your account email."),
password: z
.string({
required_error: "Password is needed.",
})
.describe("Your account password."),
});
const registerSchema = z.object({
firstName: z.string({
required_error: "First name is required.",
}).describe("First name"),
lastName: z.string({
required_error: "Last name is required.",
}).describe("Last name"),
pseudo: z.string({
required_error: "Nickname is required.",
}).describe("Nickname"),
email: z
.string({
required_error: "Email is required.",
})
.email("Email must be valid."),
password: z
.string({
required_error: "Password is required.",
})
.min(8, "Password must be at least 8 characters.")
.max(32, "Password must be less than 32 characters.")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter.")
.regex(/[a-z]/, "Password must contain at least one lowercase letter.")
.regex(/[0-9]/, "Password must contain at least one number.")
.regex(/[^a-zA-Z0-9]/, "Password must contain at least one special character.")
.describe("Your account password."),
promoCode: z
.string().max(255, "Your promotional code is too long.").optional(),
});
export function AuthForms() {
const [isLoading, setIsLoading] = useState(false);
const [sub, setSub] = useLocalStorage<string | undefined>("sub", "");
const userContext = useContext(UserDataContext);
const { toast } = useToast();
async function doRegister(
registerData: IApiRegisterReq,
userDataSetter: Dispatch<SetStateAction<IUserData | null>>,
): Promise<IStandardisedReturn<IApiRegisterRes>> {
console.trace(registerData);
try {
const ReqRes = await ApiRequest.standard.post.json<
IApiRegisterReq,
IApiRegisterRes
>("auth/sign-up", registerData);
console.trace(ReqRes.data);
if (ReqRes.data.user) {
userDataSetter({
...ReqRes.data.user,
wallet: {
uat: Date.now(),
update_interval: 30_000,
owned_cryptos: [],
},
});
setSub(ReqRes.data.access_token);
}
console.debug(ReqRes.data.message || "Not additional message from request");
return {
state: EReturnState.done,
resolved: ReqRes.data,
};
} catch (error) {
console.error("Error during registration:", error);
return {
state: EReturnState.serverError,
message: error as string,
};
}
}
async function doLogin(
loginData: IApiLoginReq,
): Promise<IStandardisedReturn<IApiLoginRes>> {
try {
const ReqRes = await ApiRequest.standard.post.json<IApiLoginReq, IApiLoginRes>(
"auth/sign-in",
loginData,
);
console.trace(ReqRes.data);
if (ReqRes.data.access_token) {
setSub(ReqRes.data.access_token);
}
return {
state: EReturnState.done,
};
} catch (err) {
console.error("Error during login:", err);
return {
state: EReturnState.serverError,
message: err as string,
};
}
}
if (!userContext || !userContext.setUserData) {
return (
<div
className={
"bg-destructive text-destructive-foreground p-3 gap-2 border rounded flex flex-row justify-center items-center"
}
>
<Bug />
<p>It seems that the context is missing..</p>
</div>
);
}
return (
<section className={"flex flex-col gap-6 items-center justify-center"}>
<Link href={"/"} className={"hidden sm:visible sm:flex flex-row justify-center md:justify-start items-center w-fit gap-2"}>
<Image src={"neptune.svg"} alt={"Logo of Neptune"} className={"w-24 h-24"} width={128} height={128} />
<h1 className={"font-bold text-xl lg:text-2xl align-middle text-center text-wrap"}>
Neptune Crypto
</h1>
</Link>
<Tabs defaultValue="login" className="w-full p-2 gap-4 flex flex-col justify-center sm:p-4 rounded bg-card text-card-foreground md:w-[400px] my-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<TabsContent value="login">
<AutoForm
// I pass the Zod schema to the form
formSchema={loginSchema}
onSubmit={(data: IApiLoginReq) => {
setIsLoading(true);
doLogin(data).then((res) => {
if (res.state !== EReturnState.done) {
toast({
description: res.message || "An unexpected error occurred..",
variant: "destructive",
});
setIsLoading(false);
return;
}
//toast.custom(<ToastBox message={"Login successful ! \n You will be redirected."} type={toastType.success}/>)
toast({
description: "Login successful ! \n You will be redirected to the home page.",
});
setTimeout(() => {
setIsLoading(false);
location.href = "/";
console.log("Moving to home.");
}, 3_000);
});
}}
fieldConfig={{
password: {
inputProps: {
type: "password",
placeholder: "••••••••",
},
},
}}
>
<AutoFormSubmit
disabled={!!isLoading}
className={"gap-2 disabled:bg-secondary"}
>
{/* biome-ignore lint/style/useTemplate: <explanation> */}
<RefreshCw className={"animate-spin" + isLoading && "hidden"} />
<p>Login</p>
</AutoFormSubmit>
</AutoForm>
</TabsContent>
<TabsContent value="register">
<AutoForm
// Pass the schema to the form
formSchema={registerSchema}
onSubmit={(data: IApiRegisterReq) => {
setIsLoading(true);
doRegister(
data,
userContext.setUserData as Dispatch<SetStateAction<IUserData | null>>,
).then((res) => {
if (res.state !== EReturnState.done) {
//toast.custom(<ToastBox message={res.message || "An unexpected error occurred.."} type={toastType.error}/>)
setIsLoading(false);
return;
}
//toast.custom(<ToastBox message={"Register successful ! \n You will be redirected."} type={toastType.success}/>)
setTimeout(() => {
setIsLoading(false);
//location.href = "/"
console.log("Moving to home.");
}, 5_000);
});
}}
fieldConfig={{
password: {
inputProps: {
type: "password",
placeholder: "••••••••",
},
},
}}
>
<AutoFormSubmit
disabled={!!isLoading}
className={"gap-2 disabled:bg-secondary"}
>
{/* biome-ignore lint/style/useTemplate: <explanation> */}
<RefreshCw className={"animate-spin" + !isLoading && "hidden"} />
<p>Register</p>
</AutoFormSubmit>
<p className="text-gray-500 text-sm">
By submitting this form, you agree to the{" "}
<Link title={"Go to legal page"} href="/legal" className="text-primary underline">
GDPR Compliance Policy & Usage Policy
</Link>
.
</p>
</AutoForm>
</TabsContent>
</Tabs>
</section>
);
}

View File

@@ -0,0 +1,23 @@
import { FormLabel } from "@/components/ui/form";
import { cn } from "@/lib/utils";
function AutoFormLabel({
label,
isRequired,
className,
}: {
label: string;
isRequired: boolean;
className?: string;
}) {
return (
<>
<FormLabel className={cn(className)}>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
</>
);
}
export default AutoFormLabel;

View File

@@ -0,0 +1,13 @@
function AutoFormTooltip({ fieldConfigItem }: { fieldConfigItem: any }) {
return (
<>
{fieldConfigItem?.description && (
<p className="text-sm text-gray-500 dark:text-white">
{fieldConfigItem.description}
</p>
)}
</>
);
}
export default AutoFormTooltip;

View File

@@ -0,0 +1,35 @@
import AutoFormCheckbox from "./fields/checkbox";
import AutoFormDate from "./fields/date";
import AutoFormEnum from "./fields/enum";
import AutoFormFile from "./fields/file";
import AutoFormInput from "./fields/input";
import AutoFormNumber from "./fields/number";
import AutoFormRadioGroup from "./fields/radio-group";
import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea";
export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox,
date: AutoFormDate,
select: AutoFormEnum,
radio: AutoFormRadioGroup,
switch: AutoFormSwitch,
textarea: AutoFormTextarea,
number: AutoFormNumber,
file: AutoFormFile,
fallback: AutoFormInput,
};
/**
* Define handlers for specific Zod types.
* You can expand this object to support more types.
*/
export const DEFAULT_ZOD_HANDLERS: {
[key: string]: keyof typeof INPUT_COMPONENTS;
} = {
ZodBoolean: "checkbox",
ZodDate: "date",
ZodEnum: "select",
ZodNativeEnum: "select",
ZodNumber: "number",
};

View File

@@ -0,0 +1,57 @@
import type { FieldValues, UseFormWatch } from "react-hook-form";
import type * as z from "zod";
import { type Dependency, DependencyType, type EnumValues } from "./types";
export default function resolveDependencies<
SchemaType extends z.infer<z.ZodObject<any, any>>,
>(
dependencies: Dependency<SchemaType>[],
currentFieldName: keyof SchemaType,
watch: UseFormWatch<FieldValues>,
) {
let isDisabled = false;
let isHidden = false;
let isRequired = false;
let overrideOptions: EnumValues | undefined;
const currentFieldValue = watch(currentFieldName as string);
const currentFieldDependencies = dependencies.filter(
(dependency) => dependency.targetField === currentFieldName,
);
for (const dependency of currentFieldDependencies) {
const watchedValue = watch(dependency.sourceField as string);
const conditionMet = dependency.when(watchedValue, currentFieldValue);
switch (dependency.type) {
case DependencyType.DISABLES:
if (conditionMet) {
isDisabled = true;
}
break;
case DependencyType.REQUIRES:
if (conditionMet) {
isRequired = true;
}
break;
case DependencyType.HIDES:
if (conditionMet) {
isHidden = true;
}
break;
case DependencyType.SETS_OPTIONS:
if (conditionMet) {
overrideOptions = dependency.options;
}
break;
}
}
return {
isDisabled,
isHidden,
isRequired,
overrideOptions,
};
}

View File

@@ -0,0 +1,91 @@
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Plus, Trash } from "lucide-react";
import { useFieldArray, type useForm } from "react-hook-form";
import * as z from "zod";
import { beautifyObjectName } from "../utils";
import AutoFormObject from "./object";
function isZodArray(item: z.ZodArray<any> | z.ZodDefault<any>): item is z.ZodArray<any> {
return item instanceof z.ZodArray;
}
function isZodDefault(
item: z.ZodArray<any> | z.ZodDefault<any>,
): item is z.ZodDefault<any> {
return item instanceof z.ZodDefault;
}
export default function AutoFormArray({
name,
item,
form,
path = [],
fieldConfig,
}: {
name: string;
item: z.ZodArray<any> | z.ZodDefault<any>;
form: ReturnType<typeof useForm>;
path?: string[];
fieldConfig?: any;
}) {
const { fields, append, remove } = useFieldArray({
control: form.control,
name,
});
const title = item._def.description ?? beautifyObjectName(name);
const itemDefType = isZodArray(item)
? item._def.type
: isZodDefault(item)
? item._def.innerType._def.type
: null;
return (
<AccordionItem value={name} className="border-none">
<AccordionTrigger>{title}</AccordionTrigger>
<AccordionContent>
{fields.map((_field, index) => {
const key = _field.id;
return (
<div className="mt-4 flex flex-col" key={`${key}`}>
<AutoFormObject
schema={itemDefType as z.ZodObject<any, any>}
form={form}
fieldConfig={fieldConfig}
path={[...path, index.toString()]}
/>
<div className="my-4 flex justify-end">
<Button
variant="secondary"
size="icon"
type="button"
className="hover:bg-zinc-300 hover:text-black focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:hover:bg-zinc-300 dark:hover:text-black dark:hover:ring-0 dark:hover:ring-offset-0 dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0"
onClick={() => remove(index)}
>
<Trash className="size-4 " />
</Button>
</div>
<Separator />
</div>
);
})}
<Button
type="button"
variant="secondary"
onClick={() => append({})}
className="mt-4 flex items-center"
>
<Plus className="mr-2" size={16} />
Add
</Button>
</AccordionContent>
</AccordionItem>
);
}

View File

@@ -0,0 +1,34 @@
import { Checkbox } from "@/components/ui/checkbox";
import { FormControl, FormItem } from "@/components/ui/form";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormCheckbox({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<div>
<FormItem>
<div className="mb-3 flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
{...fieldProps}
/>
</FormControl>
<AutoFormLabel
label={fieldConfigItem?.label || label}
isRequired={isRequired}
/>
</div>
</FormItem>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { DatePicker } from "@/components/ui/date-picker";
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormDate({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<FormItem>
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
<FormControl>
<DatePicker date={field.value} setDate={field.onChange} {...fieldProps} />
</FormControl>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
);
}

View File

@@ -0,0 +1,59 @@
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type * as z from "zod";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
import { getBaseSchema } from "../utils";
export default function AutoFormEnum({
label,
isRequired,
field,
fieldConfigItem,
zodItem,
fieldProps,
}: AutoFormInputComponentProps) {
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def.values;
let values: [string, string][] = [];
if (!Array.isArray(baseValues)) {
values = Object.entries(baseValues);
} else {
values = baseValues.map((value) => [value, value]);
}
function findItem(value: any) {
return values.find((item) => item[0] === value);
}
return (
<FormItem>
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
<FormControl>
<Select onValueChange={field.onChange} defaultValue={field.value} {...fieldProps}>
<SelectTrigger className={fieldProps.className}>
<SelectValue placeholder={fieldConfigItem.inputProps?.placeholder}>
{field.value ? findItem(field.value)?.[1] : "Select an option"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{values.map(([value, label]) => (
<SelectItem value={label} key={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
);
}

View File

@@ -0,0 +1,64 @@
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Trash2 } from "lucide-react";
import { type ChangeEvent, useState } from "react";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormFile({
label,
isRequired,
fieldConfigItem,
fieldProps,
field,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
const [file, setFile] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setFile(reader.result as string);
setFileName(file.name);
field.onChange(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const handleRemoveClick = () => {
setFile(null);
};
return (
<FormItem>
{showLabel && (
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
)}
{!file && (
<FormControl>
<Input
type="file"
{...fieldPropsWithoutShowLabel}
onChange={handleFileChange}
value={""}
/>
</FormControl>
)}
{file && (
<div className="flex h-[40px] w-full flex-row items-center justify-between space-x-2 rounded-sm border p-2 text-black focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0">
<p>{fileName}</p>
<button onClick={handleRemoveClick} aria-label="Remove image">
<Trash2 size={16} />
</button>
</div>
)}
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
);
}

View File

@@ -0,0 +1,34 @@
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormInput({
label,
isRequired,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
const type = fieldProps.type || "text";
return (
<div className="flex flex-row items-center space-x-2">
<FormItem className="flex w-full flex-col justify-start">
{showLabel && (
<AutoFormLabel
label={fieldConfigItem?.label || label}
isRequired={isRequired}
/>
)}
<FormControl>
<Input type={type} {...fieldPropsWithoutShowLabel} />
</FormControl>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormNumber({
label,
isRequired,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
return (
<FormItem>
{showLabel && (
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
)}
<FormControl>
<Input type="number" {...fieldPropsWithoutShowLabel} />
</FormControl>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
);
}

View File

@@ -0,0 +1,175 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { FormField } from "@/components/ui/form";
import { type useForm, useFormContext } from "react-hook-form";
import * as z from "zod";
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
import resolveDependencies from "../dependencies";
import type { Dependency, FieldConfig, FieldConfigItem } from "../types";
import {
beautifyObjectName,
getBaseSchema,
getBaseType,
zodToHtmlInputProps,
} from "../utils";
import AutoFormArray from "./array";
function DefaultParent({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export default function AutoFormObject<SchemaType extends z.ZodObject<any, any>>({
schema,
form,
fieldConfig,
path = [],
dependencies = [],
}: {
schema: SchemaType | z.ZodEffects<SchemaType>;
form: ReturnType<typeof useForm>;
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
path?: string[];
dependencies?: Dependency<z.infer<SchemaType>>[];
}) {
const { watch } = useFormContext(); // Use useFormContext to access the watch function
if (!schema) {
return null;
}
const { shape } = getBaseSchema<SchemaType>(schema) || {};
if (!shape) {
return null;
}
const handleIfZodNumber = (item: z.ZodAny) => {
const isZodNumber = (item as any)._def.typeName === "ZodNumber";
const isInnerZodNumber = (item._def as any).innerType?._def?.typeName === "ZodNumber";
if (isZodNumber) {
(item as any)._def.coerce = true;
} else if (isInnerZodNumber) {
(item._def as any).innerType._def.coerce = true;
}
return item;
};
return (
<Accordion type="multiple" className="space-y-5 border-none">
{Object.keys(shape).map((name) => {
let item = shape[name] as z.ZodAny;
item = handleIfZodNumber(item) as z.ZodAny;
const zodBaseType = getBaseType(item);
const itemName = item._def.description ?? beautifyObjectName(name);
const key = [...path, name].join(".");
const {
isHidden,
isDisabled,
isRequired: isRequiredByDependency,
overrideOptions,
} = resolveDependencies(dependencies, name, watch);
if (isHidden) {
return null;
}
if (zodBaseType === "ZodObject") {
return (
<AccordionItem value={name} key={key} className="border-none">
<AccordionTrigger>{itemName}</AccordionTrigger>
<AccordionContent className="p-2">
<AutoFormObject
schema={item as unknown as z.ZodObject<any, any>}
form={form}
fieldConfig={
(fieldConfig?.[name] ?? {}) as FieldConfig<z.infer<typeof item>>
}
path={[...path, name]}
/>
</AccordionContent>
</AccordionItem>
);
}
if (zodBaseType === "ZodArray") {
return (
<AutoFormArray
key={key}
name={name}
item={item as unknown as z.ZodArray<any>}
form={form}
fieldConfig={fieldConfig?.[name] ?? {}}
path={[...path, name]}
/>
);
}
const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
const zodInputProps = zodToHtmlInputProps(item);
const isRequired =
isRequiredByDependency ||
zodInputProps.required ||
fieldConfigItem.inputProps?.required ||
false;
if (overrideOptions) {
item = z.enum(overrideOptions) as unknown as z.ZodAny;
}
return (
<FormField
control={form.control}
name={key}
key={key}
render={({ field }) => {
const inputType =
fieldConfigItem.fieldType ??
DEFAULT_ZOD_HANDLERS[zodBaseType] ??
"fallback";
const InputComponent =
typeof inputType === "function" ? inputType : INPUT_COMPONENTS[inputType];
const ParentElement = fieldConfigItem.renderParent ?? DefaultParent;
const defaultValue = fieldConfigItem.inputProps?.defaultValue;
const value = field.value ?? defaultValue ?? "";
const fieldProps = {
...zodToHtmlInputProps(item),
...field,
...fieldConfigItem.inputProps,
disabled: fieldConfigItem.inputProps?.disabled || isDisabled,
ref: undefined,
value: value,
};
if (InputComponent === undefined) {
return <></>;
}
return (
<ParentElement key={`${key}.parent`}>
<InputComponent
zodInputProps={zodInputProps}
field={field}
fieldConfigItem={fieldConfigItem}
label={itemName}
isRequired={isRequired}
zodItem={item}
fieldProps={fieldProps}
className={fieldProps.className}
/>
</ParentElement>
);
}}
/>
);
})}
</Accordion>
);
}

View File

@@ -0,0 +1,51 @@
import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import type * as z from "zod";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
import { getBaseSchema } from "../utils";
export default function AutoFormRadioGroup({
label,
isRequired,
field,
zodItem,
fieldProps,
fieldConfigItem,
}: AutoFormInputComponentProps) {
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def.values;
let values: string[] = [];
if (!Array.isArray(baseValues)) {
values = Object.entries(baseValues).map((item) => item[0]);
} else {
values = baseValues;
}
return (
<div>
<FormItem>
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
{...fieldProps}
>
{values?.map((value: any) => (
<FormItem key={value} className="mb-2 flex items-center gap-3 space-y-0">
<FormControl>
<RadioGroupItem value={value} />
</FormControl>
<FormLabel className="font-normal">{value}</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { FormControl, FormItem } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormSwitch({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<div>
<FormItem>
<div className="flex items-center gap-3">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
{...fieldProps}
/>
</FormControl>
<AutoFormLabel
label={fieldConfigItem?.label || label}
isRequired={isRequired}
/>
</div>
</FormItem>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import AutoFormLabel from "../common/label";
import AutoFormTooltip from "../common/tooltip";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormTextarea({
label,
isRequired,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
return (
<FormItem>
{showLabel && (
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
)}
<FormControl>
<Textarea {...fieldPropsWithoutShowLabel} />
</FormControl>
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
<FormMessage />
</FormItem>
);
}

View File

@@ -0,0 +1,117 @@
"use client";
import { Form } from "@/components/ui/form";
import type React from "react";
import { useEffect } from "react";
import { type DefaultValues, type FormState, useForm } from "react-hook-form";
import type { z } from "zod";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import AutoFormObject from "@/components/auto-form/fields/object";
import type { Dependency, FieldConfig } from "@/components/auto-form/types";
import {
type ZodObjectOrWrapped,
getDefaultValues,
getObjectFormSchema,
} from "@/components/auto-form/utils";
export function AutoFormSubmit({
children,
className,
disabled,
}: {
children?: React.ReactNode;
className?: string;
disabled?: boolean;
}) {
return (
<Button type="submit" disabled={disabled} className={className}>
{children ?? "Submit"}
</Button>
);
}
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
formSchema,
values: valuesProp,
onValuesChange: onValuesChangeProp,
onParsedValuesChange,
onSubmit: onSubmitProp,
fieldConfig,
children,
className,
dependencies,
}: {
formSchema: SchemaType;
values?: Partial<z.infer<SchemaType>>;
onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
onParsedValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
onSubmit?: (values: z.infer<SchemaType>) => void;
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
children?:
| React.ReactNode
| ((formState: FormState<z.infer<SchemaType>>) => React.ReactNode);
className?: string;
dependencies?: Dependency<z.infer<SchemaType>>[];
}) {
const objectFormSchema = getObjectFormSchema(formSchema);
const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> | null =
getDefaultValues(objectFormSchema, fieldConfig);
const form = useForm<z.infer<typeof objectFormSchema>>({
resolver: zodResolver(formSchema),
defaultValues: defaultValues ?? undefined,
values: valuesProp,
});
function onSubmit(values: z.infer<typeof formSchema>) {
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
onSubmitProp?.(parsedValues.data);
}
}
const values = form.watch();
// valuesString is needed because form.watch() returns a new object every time
const valuesString = JSON.stringify(values);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
onValuesChangeProp?.(values);
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
onParsedValuesChange?.(parsedValues.data);
}
}, [valuesString]);
const renderChildren =
typeof children === "function"
? children(form.formState as FormState<z.infer<SchemaType>>)
: children;
return (
<div className="w-full">
<Form {...form}>
<form
onSubmit={(e) => {
form.handleSubmit(onSubmit)(e);
}}
className={cn("space-y-5 w-full", className)}
>
<AutoFormObject
schema={objectFormSchema}
form={form}
dependencies={dependencies}
fieldConfig={fieldConfig}
/>
{renderChildren}
</form>
</Form>
</div>
);
}
export default AutoForm;

View File

@@ -0,0 +1,71 @@
import type React from "react";
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
import type * as z from "zod";
import type { INPUT_COMPONENTS } from "./config";
export type FieldConfigItem = {
description?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
showLabel?: boolean;
};
label?: string;
fieldType?: keyof typeof INPUT_COMPONENTS | React.FC<AutoFormInputComponentProps>;
renderParent?: (props: {
children: React.ReactNode;
}) => React.ReactElement | null;
};
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
[Key in keyof SchemaType]?: SchemaType[Key] extends object
? FieldConfig<z.infer<SchemaType[Key]>>
: FieldConfigItem;
};
export enum DependencyType {
DISABLES = 0,
REQUIRES = 1,
HIDES = 2,
SETS_OPTIONS = 3,
}
type BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
sourceField: keyof SchemaType;
type: DependencyType;
targetField: keyof SchemaType;
when: (sourceFieldValue: any, targetFieldValue: any) => boolean;
};
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
BaseDependency<SchemaType> & {
type: DependencyType.DISABLES | DependencyType.REQUIRES | DependencyType.HIDES;
};
export type EnumValues = readonly [string, ...string[]];
export type OptionsDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
BaseDependency<SchemaType> & {
type: DependencyType.SETS_OPTIONS;
// Partial array of values from sourceField that will trigger the dependency
options: EnumValues;
};
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
| ValueDependency<SchemaType>
| OptionsDependency<SchemaType>;
/**
* A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
*/
export type AutoFormInputComponentProps = {
zodInputProps: React.InputHTMLAttributes<HTMLInputElement>;
field: ControllerRenderProps<FieldValues, any>;
fieldConfigItem: FieldConfigItem;
label: string;
isRequired: boolean;
fieldProps: any;
zodItem: z.ZodAny;
className?: string;
};

View File

@@ -0,0 +1,166 @@
import type React from "react";
import type { DefaultValues } from "react-hook-form";
import type { z } from "zod";
import type { FieldConfig } from "./types";
export type ZodObjectOrWrapped =
| z.ZodObject<any, any>
| z.ZodEffects<z.ZodObject<any, any>>;
/**
* Beautify a camelCase string.
* e.g. "myString" -> "My String"
*/
export function beautifyObjectName(string: string) {
// if numbers only return the string
let output = string.replace(/([A-Z])/g, " $1");
output = output.charAt(0).toUpperCase() + output.slice(1);
return output;
}
/**
* Get the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseSchema<ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny>(
schema: ChildType | z.ZodEffects<ChildType>,
): ChildType | null {
if (!schema) return null;
if ("innerType" in schema._def) {
return getBaseSchema(schema._def.innerType as ChildType);
}
if ("schema" in schema._def) {
return getBaseSchema(schema._def.schema as ChildType);
}
return schema as ChildType;
}
/**
* Get the type name of the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseType(schema: z.ZodAny): string {
const baseSchema = getBaseSchema(schema);
return baseSchema ? baseSchema._def.typeName : "";
}
/**
* Search for a "ZodDefult" in the Zod stack and return its value.
*/
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
const typedSchema = schema as unknown as z.ZodDefault<z.ZodNumber | z.ZodString>;
if (typedSchema._def.typeName === "ZodDefault") {
return typedSchema._def.defaultValue();
}
if ("innerType" in typedSchema._def) {
return getDefaultValueInZodStack(typedSchema._def.innerType as unknown as z.ZodAny);
}
if ("schema" in typedSchema._def) {
return getDefaultValueInZodStack((typedSchema._def as any).schema as z.ZodAny);
}
return undefined;
}
/**
* Get all default values from a Zod schema.
*/
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
schema: Schema,
fieldConfig?: FieldConfig<z.infer<Schema>>,
) {
if (!schema) return null;
const { shape } = schema;
type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>;
const defaultValues = {} as DefaultValuesType;
if (!shape) return defaultValues;
for (const key of Object.keys(shape)) {
const item = shape[key] as z.ZodAny;
if (getBaseType(item) === "ZodObject") {
const defaultItems = getDefaultValues(
getBaseSchema(item) as unknown as z.ZodObject<any, any>,
fieldConfig?.[key] as FieldConfig<z.infer<Schema>>,
);
if (defaultItems !== null) {
for (const defaultItemKey of Object.keys(defaultItems)) {
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
defaultValues[pathKey] = defaultItems[defaultItemKey];
}
}
} else {
let defaultValue = getDefaultValueInZodStack(item);
if (
(defaultValue === null || defaultValue === "") &&
fieldConfig?.[key]?.inputProps
) {
defaultValue = (fieldConfig?.[key]?.inputProps as unknown as any).defaultValue;
}
if (defaultValue !== undefined) {
defaultValues[key as keyof DefaultValuesType] = defaultValue;
}
}
}
return defaultValues;
}
export function getObjectFormSchema(schema: ZodObjectOrWrapped): z.ZodObject<any, any> {
if (schema?._def.typeName === "ZodEffects") {
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
return getObjectFormSchema(typedSchema._def.schema);
}
return schema as z.ZodObject<any, any>;
}
/**
* Convert a Zod schema to HTML input props to give direct feedback to the user.
* Once submitted, the schema will be validated completely.
*/
export function zodToHtmlInputProps(
schema: z.ZodNumber | z.ZodString | z.ZodOptional<z.ZodNumber | z.ZodString> | any,
): React.InputHTMLAttributes<HTMLInputElement> {
if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
return {
...zodToHtmlInputProps(typedSchema._def.innerType),
required: false,
};
}
const typedSchema = schema as z.ZodNumber | z.ZodString;
if (!("checks" in typedSchema._def))
return {
required: true,
};
const { checks } = typedSchema._def;
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
required: true,
};
const type = getBaseType(schema);
for (const check of checks) {
if (check.kind === "min") {
if (type === "ZodString") {
inputProps.minLength = check.value;
} else {
inputProps.min = check.value;
}
}
if (check.kind === "max") {
if (type === "ZodString") {
inputProps.maxLength = check.value;
} else {
inputProps.max = check.value;
}
}
}
return inputProps;
}

View File

@@ -0,0 +1,243 @@
import AutoForm, { AutoFormSubmit } from "@/components/auto-form";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/use-toast";
import type { IApiAllOffersRes } from "@/interfaces/api.interface";
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
import ApiRequest from "@/services/apiRequest";
import { zodResolver } from "@hookform/resolvers/zod";
import { Ban, DollarSign, RefreshCw } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
type Props = {
cryptoData: ICryptoInWalletInfo;
};
export function BuyModal(props: Props) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [offersList, setOffersList] = useState<IApiAllOffersRes[]>([]);
const [isLoaded, setIsLoaded] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
if (!isLoaded) {
ApiRequest.authenticated.get
.json<IApiAllOffersRes[]>(`offer/crypto/${props.cryptoData.id}`)
.then((response) => {
console.debug(response)
if (response.data) {setOffersList(response.data)}
console.log(`Crypto ${props.cryptoData.name} -> ${response.data.length}`);
setIsLoaded(true);
return;
});
}
}, [isLoaded]);
const buyFromServerSchema = z.object({
amount: z
.number({
required_error: "An amount is needed.",
})
.min(1)
.max(
props.cryptoData.quantity,
"You cant buy more that what is available on the server.",
)
.describe("The amount you want to buy."),
});
const buyFromUserSchema = z.object({
offerId: z
.string({
required_error: "You should select an offer.",
})
.uuid(),
});
function onBuyFromServerSubmit(data: z.infer<typeof buyFromServerSchema>) {
ApiRequest.authenticated.post
.json("crypto/buy", { id_crypto: props.cryptoData.id, amount: data.amount })
.then((res) => {
if (res.status !== 201) {
toast({
title: "An error occurred !",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white text-wrap">
{JSON.stringify(res.statusText, null, 2)}
</code>
</pre>
),
});
return;
}
toast({
title: "Transaction accepted.",
description: <p>You will be redirected.</p>,
});
setTimeout(() => location.reload(), 1_500);
});
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
function onBuyFromUserSubmit(data: z.infer<typeof buyFromUserSchema>) {
ApiRequest.authenticated.post
.json("trade/create", { id_offer: data.offerId })
.then((res) => {
if (res.status !== 201) {
toast({
title: "An error occurred !",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white text-wrap">
{JSON.stringify(res.statusText, null, 2)}
</code>
</pre>
),
});
return;
}
toast({
title: "Transaction accepted.",
description: <p>You will be redirected.</p>,
});
setTimeout(() => location.reload(), 1_500);
});
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
const buyFromUserForm = useForm<z.infer<typeof buyFromUserSchema>>({
resolver: zodResolver(buyFromUserSchema),
});
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="light">
<DollarSign />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[800px] flex flex-col justify-start items-center">
<Tabs defaultValue="server" className="w-full p-2 my-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="user">
Buy from user{" "}
<p className={" ml-1 px-1 bg-primary text-primary-foreground rounded"}>
{offersList.length}
</p>
</TabsTrigger>
<TabsTrigger value="server">Buy from server{" "}<p
className={" ml-1 px-1 bg-primary text-primary-foreground rounded"}>
{props.cryptoData.quantity}
</p></TabsTrigger>
</TabsList>
<TabsContent
value="user"
className={"flex flex-col justify-start items-center"}
>
<Form {...buyFromUserForm}>
<form
onSubmit={buyFromUserForm.handleSubmit(onBuyFromUserSubmit)}
className="w-full space-y-6"
>
<FormField
control={buyFromUserForm.control}
name="offerId"
render={({ field }) => (
<FormItem>
<FormLabel>Select an offer</FormLabel>
<Select defaultValue={offersList[0]?.id} onValueChange={(val) => {
console.log(val);
field.onChange(val);
}}>
<SelectTrigger>
<SelectValue placeholder="Select an offer to purchase."/>
</SelectTrigger>
<SelectContent>
{offersList.map((offer) => (
<SelectItem
value={offer.id}
key={offer.id}
>{`${offer.amount}x ${offer.Crypto.name}`}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</TabsContent>
<TabsContent
value="server"
className={"flex flex-col justify-start items-center"}
>
{!props.cryptoData.quantity && (
<div
className={
"bg-destructive text-destructive-foreground border-destructive rounded p-2 flex justify-start items-center gap-2"
}
>
<Ban />
<p>The server dont have stock for the designated cryptos.</p>
</div>
)}
<div className={"w-full flex justify-center gap-2 items-center p-2"}>
<p>Available quantity on the server :</p>
<p className={"p-1 bg-accent text-accent-foreground rounded m-1"}>
{props.cryptoData.quantity}
</p>
</div>
{props.cryptoData.quantity && (
<AutoForm
// Pass the schema to the form
formSchema={buyFromServerSchema}
onSubmit={onBuyFromServerSubmit}
>
<AutoFormSubmit
disabled={isLoading}
className={"gap-2 disabled:bg-secondary-foreground"}
>
<RefreshCw className={`animate-spin ${!isLoading && "hidden"}`} />
<p>Buy from the server</p>
</AutoFormSubmit>
</AutoForm>
)}
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,10 @@
import type {Row} from "@tanstack/react-table";
import type {ICryptoInUserWalletInfo} from "@/interfaces/crypto.interface";
interface SellModalProps {
row: Row<ICryptoInUserWalletInfo>
}
export function SellModal({ row }: SellModalProps) {
}

View File

@@ -0,0 +1,32 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { LineChart } from "lucide-react";
// @flow
import * as React from "react";
type Props = {
targetedCryptoId: string;
};
export function ViewModal(props: Props) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost">
<LineChart />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[800px]">
Test 1,2 ! //From here
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,85 @@
"use client";
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
import * as React from "react";
import { BuyModal } from "@/components/cryptos/buy-modal";
import { ViewModal } from "@/components/cryptos/view-modal";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/ui/data-table";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
type Props = {
cryptosArray: ICryptoInWalletInfo[];
};
export function CryptosTable(props: Props) {
const router = useRouter();
const cryptos = props.cryptosArray;
const columns: ColumnDef<ICryptoInWalletInfo, any>[] = [
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "value",
header: ({ column }) => {
return (
<Button
variant="light"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Value - USD
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
{
accessorKey: "quantity",
header: ({ column }) => {
return (
<Button
variant="light"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Available from server
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
{
id: "actions",
cell: ({ row }) => {
const payment = row.original;
return (
<div className={"flex gap-2"}>
<BuyModal cryptoData={row.original} />
<ViewModal targetedCryptoId={row.original.id} />
</div>
);
},
},
];
return (
<>
<DataTable columns={columns} data={cryptos} fieldToFilter={"name"} />
</>
);
}

View File

@@ -0,0 +1,85 @@
"use client";
import type {
ICryptoInUserWalletInfo,
ICryptoInWalletInfo,
} from "@/interfaces/crypto.interface";
import * as React from "react";
import { BuyModal } from "@/components/cryptos/buy-modal";
import { ViewModal } from "@/components/cryptos/view-modal";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/ui/data-table";
import type { IUserWallet } from "@/interfaces/userdata.interface";
import type {
ColumnDef,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal, Receipt } from "lucide-react";
import { useRouter } from "next/navigation";
import Link from "next/link";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
type Props = {
walletArray: IUserWallet;
};
export function WalletTable(props: Props) {
const router = useRouter();
const wallet = props.walletArray.owned_cryptos;
const columns: ColumnDef<ICryptoInUserWalletInfo, any>[] = [
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "value",
header: ({ column }) => {
return (
<Button
variant="light"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Value - USD
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
{
accessorKey: "owned_amount",
header: ({ column }) => {
return (
<Button
variant="light"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Amount owned
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
{
id: "actions",
cell: ({ row }) => {
const payment = row.original;
return (
<div>
<Link href={`/sell/${row.original.id}`} className={"flex w-fit gap-2 p-2 justify-center items-center rounded hover:bg-card"}><Receipt /> <p className={"hidden sm:block"}>Sell for USD</p></Link>
</div>
);
},
},
];
return (
<>
<DataTable columns={columns} data={wallet} fieldToFilter={"name"} />
</>
);
}

55
src/components/footer.tsx Normal file
View File

@@ -0,0 +1,55 @@
import { Copyright } from "lucide-react";
import Link from "next/link";
export function Footer() {
return (
<footer
className={
"flex flex-col-reverse md:flex-row justify-between gap-2 md:gap-1 self-end order-6 items-center p-2 border-t-2 w-full"
}
>
<div
className={
"flex flex-row gap-1 items-center justify-center md:justify-start md:w-1/3 opacity-50"
}
>
<Copyright className={"w-4"} />
<h4 className={"pr-2"}>Yidhra Studio</h4>
<p>
MIT <em>2024</em>
</p>
</div>
<div
className={
"flex flex-col flex-wrap md:flex-row max-h-24 md:flex-nowrap gap-1 md:gap-2 items-center justify-evenly w-full md:w-1/3"
}
>
<Link
href={"#"}
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
>
<h3 className={"text-nowrap text-center"}>Data privacy</h3>
</Link>
<Link
href={"#"}
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
>
<h3 className={"text-nowrap text-center"}>Terms and conditions</h3>
</Link>
<Link
href={"#"}
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
>
<h3 className={"text-nowrap text-center"}>Legal notice</h3>
</Link>
<Link
href={"#"}
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
>
<h3 className={"text-nowrap text-center"}>Support Center</h3>
</Link>
</div>
<div />
</footer>
);
}

38
src/components/header.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { AccountDialog } from "@/components/account-dialog";
import { ThemeBtnSelector } from "@/components/theme-btn-selector";
import Image from "next/image";
import type React from "react";
import Link from "next/link";
export function Header({
title,
children,
}: { title?: string; children?: React.ReactNode }) {
return (
<header
className={
"flex flex-col md:flex-row justify-between items-center w-full p-1 md:px-3 gap-2 md:py-2 pb-2 border-b-2"
}
>
<Link title={"Return to home page"} href={"/"} className={"flex flex-row justify-center md:justify-start items-center w-fit gap-2"}>
<Image src={"/neptune.svg"} alt={"Logo of Neptune"} width={42} height={42} />
<h1 className={"font-bold text-xl align-middle text-center text-wrap"}>
{title || "Neptune"}
</h1>
</Link>
<div
className={"flex flex-col md:flex-row w-fit justify-center items-center"}
>
{children}
</div>
<div
className={
"w-1/3 flex flex-row justify-center md:justify-end md:w-fit gap-2 items-center"
}
>
<AccountDialog />
<ThemeBtnSelector />
</div>
</header>
);
}

View File

@@ -0,0 +1,154 @@
"use client";
import Link from "next/link";
import * as React from "react";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { cn } from "@/lib/utils";
import { Boxes, Info } from "lucide-react";
import Image from "next/image";
const components: { title: string; href: string; description: string }[] = [
{
title: "Alert Dialog",
href: "/docs/primitives/alert-dialog",
description:
"A modal dialog that interrupts the user with important content and expects a response.",
},
{
title: "Hover Card",
href: "/docs/primitives/hover-card",
description: "For sighted users to preview content available behind a link.",
},
{
title: "Progress",
href: "/docs/primitives/progress",
description:
"Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
},
{
title: "Scroll-area",
href: "/docs/primitives/scroll-area",
description: "Visually or semantically separates content.",
},
{
title: "Tabs",
href: "/docs/primitives/tabs",
description:
"A set of layered sections of content—known as tab panels—that are displayed one at a time.",
},
{
title: "Tooltip",
href: "/docs/primitives/tooltip",
description:
"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
},
];
export function PrimaryNavigationMenu() {
return (
<NavigationMenu>
<NavigationMenuList className={"flex flex-row flex-wrap md:flex-nowrap"}>
<NavigationMenuItem className={"relative"}>
<NavigationMenuTrigger className={"gap-1"}>
<Info className={"w-4"} />
Getting started
</NavigationMenuTrigger>
<NavigationMenuContent className={""}>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li className="row-span-3">
<NavigationMenuLink asChild>
<a
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href="/"
>
<Image
src={"logo-red.svg"}
alt={"Logo of Yidhra Studio"}
width={64}
height={64}
/>
<div className="mb-2 mt-4 text-lg font-medium">shadcn/ui</div>
<p className="text-sm leading-tight text-muted-foreground">
Beautifully designed components that you can copy and paste into
your apps. Accessible. Customizable. Open Source.
</p>
</a>
</NavigationMenuLink>
</li>
<ListItem href="/docs" title="Introduction">
Re-usable components built using Radix UI and Tailwind CSS.
</ListItem>
<ListItem href="/docs/installation" title="Installation">
How to install dependencies and structure your app.
</ListItem>
<ListItem href="/docs/primitives/typography" title="Typography">
Styles for headings, paragraphs, lists...etc
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger className={"gap-1"}>
<Boxes className={"w-4"} />
Features
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{components.map((component) => (
<ListItem
key={component.title}
title={component.title}
href={component.href}
>
{component.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Documentation
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);
}
const ListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
});
ListItem.displayName = "ListItem";

View File

@@ -0,0 +1,19 @@
"use client";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { UserDataProvider } from "@/components/providers/userdata-provider";
import type React from "react";
import {QueryProvider} from "@/components/providers/query-provider";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryProvider>
<UserDataProvider>
{children}
</UserDataProvider>
</QueryProvider>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,13 @@
"use client"
import type {ReactNode} from "react";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
interface QueryProviderProps {
children: ReactNode;
}
export function QueryProvider({ children }: QueryProviderProps) {
return (<QueryClientProvider client={new QueryClient()}>
{ children }
</QueryClientProvider>)
}

View File

@@ -0,0 +1,9 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,25 @@
import type { IUserData } from "@/interfaces/userdata.interface";
import { useEncodedLocalStorage } from "@/services/localStorage";
import React from "react";
export interface IUserDataProvider {
userData: IUserData | undefined;
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
}
export const UserDataContext = React.createContext<IUserDataProvider | undefined>(
undefined,
);
export const UserDataProvider = ({ children }: { children: React.ReactNode }) => {
const [userData, setUserData] = useEncodedLocalStorage<IUserData | undefined>(
"user_data",
undefined,
);
return (
<UserDataContext.Provider value={{ userData, setUserData }}>
{children}
</UserDataContext.Provider>
);
};

View File

@@ -0,0 +1,199 @@
"use client";
import * as z from "zod";
import ApiRequest from "@/services/apiRequest";
import { toast } from "@/components/ui/use-toast";
import * as React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import type { IUserWalletCryptos } from "@/interfaces/crypto.interface";
import type { IApiUserAssetsRes } from "@/interfaces/api.interface";
import { Slider } from "@/components/ui/slider";
import { DollarSign, Equal, X } from "lucide-react";
import { Card } from "@/components/ui/card";
interface SellFormProps {
defaultCryptoId?: string;
}
export function SellForm({ defaultCryptoId }: SellFormProps) {
const [currentWallet, setCurrentWallet] = useState<IUserWalletCryptos[]>();
const [sliderValue, setSliderValue] = useState(0);
const [quantity, setQuantity] = useState(0);
const [selectedCrypto, setSelectedCrypto] = useState<string>();
useEffect(() => {
async function fetchAssets() {
const res = await ApiRequest.authenticated.get.json<IApiUserAssetsRes>("user/my-assets");
setCurrentWallet(res.data.UserHasCrypto);
}
fetchAssets();
}, []);
useEffect(() => {
if (defaultCryptoId) {
const maxShares = getMaxSharesForCrypto(defaultCryptoId);
setSliderValue(maxShares);
setSelectedCrypto(defaultCryptoId);
setQuantity(1);
}
}, []);
function getMaxSharesForCrypto(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.amount || 0;
}
function getCryptoValue(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.Crypto.value || 0;
}
function getCryptoName(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.Crypto.name || "";
}
const sellToUserSchema = z.object({
cryptoId: z.string({ required_error: "You should select a crypto from your wallet." }).uuid(),
amount: z.number({
required_error: "You should select an amount of the assets that you want to sell.",
}).max(sliderValue),
});
async function onSellToUserSubmit(data: z.infer<typeof sellToUserSchema>) {
const res = await ApiRequest.authenticated.post.json("offer/create", {
id_crypto: data.cryptoId,
amount: data.amount,
});
if (res.status !== 201) {
toast({
title: "An error occurred!",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white text-wrap">{JSON.stringify(res.statusText, null, 2)}</code>
</pre>
),
});
return;
}
console.log(res);
toast({
title: "Transaction accepted.",
description: <p>The page is going to reload.</p>,
});
setTimeout(() => location.reload(), 1_500);
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
const sellToUserForm = useForm<z.infer<typeof sellToUserSchema>>({
resolver: zodResolver(sellToUserSchema),
});
if (!currentWallet) {
return (
<Card>
<div className="flex items-center justify-center p-2">
<p>Loading...</p>
</div>
</Card>
);
}
return (
<Form {...sellToUserForm}>
<form onSubmit={sellToUserForm.handleSubmit(onSellToUserSubmit)} className="w-full space-y-6">
<FormField
control={sellToUserForm.control}
name="cryptoId"
render={({ field }) => (
<FormItem
onChange={(event) => {
// @ts-ignore
const maxShares = getMaxSharesForCrypto(event.target.value as string);
setSliderValue(maxShares);
setQuantity(maxShares);
// @ts-ignore
setSelectedCrypto(event.target.value as string);
}}
>
<FormLabel>Sell shares of your wallet</FormLabel>
<Select
defaultValue={defaultCryptoId}
onValueChange={(val) => {
const maxShares = getMaxSharesForCrypto(val);
setSliderValue(maxShares);
setQuantity(1);
setSelectedCrypto(val);
field.onChange(val);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a crypto from your wallet." />
</SelectTrigger>
</FormControl>
<SelectContent>
{currentWallet?.map((crypto) => (<SelectItem value={crypto.Crypto.id} key={crypto.Crypto.id}>
{`${crypto.amount}x ${crypto.Crypto.name} - ${Math.round(crypto.Crypto.value)}$/u`}
</SelectItem>))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={sellToUserForm.control}
name="amount"
render={({ field }) => (
<FormItem hidden={!selectedCrypto}>
<FormLabel>Quantity</FormLabel>
<div className="flex items-center justify-between gap-2 p-2">
<p className="bg-accent text-accent-foreground p-1 rounded">{field.value}</p>
<Slider
form="amount"
onValueChange={(val) => {
const singleValue = val[0];
setQuantity(singleValue as number);
field.onChange(singleValue);
}}
value={[quantity]}
min={1}
max={sliderValue}
step={1}
/>
</div>
<FormMessage />
</FormItem>
)}
/>
{selectedCrypto && (
<div className="flex items-center justify-center p-2 gap-2">
<p>{getCryptoName(selectedCrypto)}</p>
<X />
<p>{quantity}</p>
<Equal />
<em className="bg-secondary/35 p-1 rounded">
{(quantity * getCryptoValue(selectedCrypto)).toLocaleString("en-US")}
</em>
<DollarSign />
</div>
)}
<Button type="submit" disabled={!quantity}>
Place sell order
</Button>
</form>
</Form>
);
}

View File

View File

@@ -0,0 +1,50 @@
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {ScrollArea} from "@/components/ui/scroll-area";
import type {IAllTrades} from "@/interfaces/crypto.interface";
import {cn} from "@/lib/utils";
interface OfferListProps {
className?: string;
trades: IAllTrades;
}
interface RenderRowsProps {
data?: IAllTrades;
}
function RenderRows({ data }: RenderRowsProps) {
if (!data || data?.length === 0) return (<TableRow>
<TableCell className="font-medium">{" "}</TableCell>
<TableCell>{" "}</TableCell>
<TableCell>{"No history..."}</TableCell>
<TableCell className="text-right">{" "}</TableCell>
</TableRow>);
return data.map((item, index) => (
<TableRow key={item.Crypto.created_at.toString()}>
<TableCell className="font-medium">{item.Giver.pseudo}</TableCell>
<TableCell>{item.Receiver.pseudo}</TableCell>
<TableCell>{item.Crypto.quantity}</TableCell>
<TableCell className="text-right">${item.Crypto.value}</TableCell>
</TableRow>
));
}
export function OfferList({ className, trades}: OfferListProps) {
return (<div className={cn("bg-card text-card-foreground rounded p-2", className)}>
<Table>
<TableCaption>A list of recent sell offers.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Seller</TableHead>
<TableHead>Buyer</TableHead>
<TableHead>Crypto share amount</TableHead>
<TableHead className="text-right">$</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<RenderRows data={trades}/>
</TableBody>
</Table>
</div>)
}

View File

@@ -0,0 +1,34 @@
"use client";
import { MoonStar, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeBtnSelector() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonStar className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,54 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -0,0 +1,133 @@
"use client";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props}
/>
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -0,0 +1,55 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,7 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View File

@@ -0,0 +1,50 @@
"use client";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -0,0 +1,34 @@
import { type VariantProps, cva } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,112 @@
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
),
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -0,0 +1,251 @@
"use client";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{ orientation = "horizontal", opts, setApi, plugins, className, children, ...props },
ref,
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
});
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
});
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
});
CarouselNext.displayName = "CarouselNext";
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

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

View File

@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

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

View File

@@ -0,0 +1,196 @@
"use client";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -0,0 +1,112 @@
"use client";
import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { Button, type ButtonProps } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useCallback, useEffect, useState } from "react";
interface CopyButtonProps extends ButtonProps {
value: string;
src?: string;
event?: Event["NONE"];
}
interface Value {
data: string;
title: string;
}
export async function copyToClipboardWithMeta(value: string) {
await window?.navigator.clipboard.writeText(value);
}
export function CopyButton({
value,
className,
src,
variant = "ghost",
event,
...props
}: CopyButtonProps) {
const [hasCopied, setHasCopied] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
return (
<Button
size="icon"
variant={variant}
className={cn(
"relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50 [&_svg]:size-3",
className,
)}
onClick={() => {
copyToClipboardWithMeta(value).then(() => setHasCopied(true));
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
);
}
interface CopyMultipleChoiceButtonProps extends DropdownMenuTriggerProps {
values: Value[];
}
export function CopyMultipleChoiceButton({
values,
className,
...props
}: CopyMultipleChoiceButtonProps) {
const [hasCopied, setHasCopied] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
const copyCommand = useCallback((value: string) => {
copyToClipboardWithMeta(value).then(() => setHasCopied(true));
}, []);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50",
className,
)}
>
{hasCopied ? (
<CheckIcon className="h-3 w-3" />
) : (
<ClipboardIcon className="h-3 w-3" />
)}
<span className="sr-only">Copy</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => copyCommand("npm")}>npm</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,129 @@
"use client";
import {
type ColumnDef,
type ColumnFiltersState,
type SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useState } from "react";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
fieldToFilter: string;
}
export function DataTable<TData, TValue>({
columns,
data,
fieldToFilter,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
});
return (
<div className={"w-full"}>
<div className="flex items-center py-4">
<Input
placeholder={`Filter ${fieldToFilter}...`}
value={(table.getColumn(fieldToFilter)?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn(fieldToFilter)?.setFilterValue(event.target.value)
}
className="max-w-sm bg-card text-card-foreground"
/>
</div>
<div className="rounded-md border w-full text-md bg-card">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className={"font-bold text-lg text-center"}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={"text-center"}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
"use client";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
export const DatePicker = forwardRef<
HTMLDivElement,
{
date?: Date;
setDate: (date?: Date) => void;
}
>(function DatePickerCmp({ date, setDate }, ref) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" ref={ref}>
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
);
});

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
import React, { useCallback, useEffect, useRef, useState } from "react";
export const FlipWords = ({
words,
duration = 3000,
className,
}: {
words: string[];
duration?: number;
className?: string;
}) => {
const [currentWord, setCurrentWord] = useState(words[0]);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
const startAnimation = useCallback(() => {
const word = words[words.indexOf(currentWord) + 1] || words[0];
setCurrentWord(word);
setIsAnimating(true);
}, [currentWord, words]);
useEffect(() => {
if (!isAnimating)
setTimeout(() => {
startAnimation();
}, duration);
}, [isAnimating, duration, startAnimation]);
return (
<AnimatePresence
onExitComplete={() => {
setIsAnimating(false);
}}
>
<motion.div
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
duration: 0.4,
ease: "easeInOut",
type: "spring",
stiffness: 100,
damping: 10,
}}
exit={{
opacity: 0,
y: -40,
x: 40,
filter: "blur(8px)",
scale: 2,
position: "absolute",
}}
className={cn(
"z-10 inline-block relative text-left text-neutral-900 dark:text-neutral-100 px-2",
className,
)}
key={currentWord}
>
{currentWord.split("").map((letter, index) => (
<motion.span
key={currentWord + index}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: index * 0.08,
duration: 0.4,
}}
className="inline-block"
>
{letter}
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};

173
src/components/ui/form.tsx Normal file
View File

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

View File

@@ -0,0 +1,136 @@
"use client";
import {
type MotionValue,
motion,
useScroll,
useSpring,
useTransform,
} from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import React from "react";
interface HeroParallaxProps {
products: product[];
children?: React.ReactNode;
}
interface product {
title: string;
link: string;
thumbnail: string;
}
export const HeroParallax = ({ products, children }: HeroParallaxProps) => {
const firstRow = products.slice(0, 5);
const secondRow = products.slice(5, 10);
const thirdRow = products.slice(10, 15);
const ref = React.useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const springConfig = { stiffness: 300, damping: 30, bounce: 100 };
const translateX = useSpring(
useTransform(scrollYProgress, [0, 1], [0, 1000]),
springConfig,
);
const translateXReverse = useSpring(
useTransform(scrollYProgress, [0, 1], [0, -1000]),
springConfig,
);
const rotateX = useSpring(
useTransform(scrollYProgress, [0, 0.2], [15, 0]),
springConfig,
);
const opacity = useSpring(
useTransform(scrollYProgress, [0, 0.2], [0.2, 1]),
springConfig,
);
const rotateZ = useSpring(
useTransform(scrollYProgress, [0, 0.2], [20, 0]),
springConfig,
);
const translateY = useSpring(
useTransform(scrollYProgress, [0, 0.2], [-700, 500]),
springConfig,
);
return (
<div
ref={ref}
className="h-[300vh] py-40 overflow-hidden antialiased relative flex flex-col self-auto [perspective:1000px] [transform-style:preserve-3d]"
>
{children}
<motion.div
style={{
rotateX,
rotateZ,
translateY,
opacity,
}}
className=""
>
<motion.div className="flex flex-row-reverse space-x-reverse space-x-20 mb-20">
{firstRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
<motion.div className="flex flex-row mb-20 space-x-20 ">
{secondRow.map((product) => (
<ProductCard
product={product}
translate={translateXReverse}
key={product.title}
/>
))}
</motion.div>
<motion.div className="flex flex-row-reverse space-x-reverse space-x-20">
{thirdRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
</motion.div>
</div>
);
};
export const ProductCard = ({
product,
translate,
}: {
product: {
title: string;
link: string;
thumbnail: string;
};
translate: MotionValue<number>;
}) => {
return (
<motion.div
style={{
x: translate,
}}
whileHover={{
y: -20,
}}
key={product.title}
className="group/product h-96 w-[30rem] relative flex-shrink-0"
>
<Link href={product.link} className="block group-hover/product:shadow-2xl ">
<Image
src={product.thumbnail}
height="600"
width="600"
className="object-cover object-left-top absolute h-full w-full rounded inset-0"
alt={product.title}
/>
</Link>
<div className="absolute inset-0 h-full w-full opacity-0 group-hover/product:opacity-80 bg-black pointer-events-none"></div>
<h2 className="absolute bottom-4 left-4 opacity-0 group-hover/product:opacity-100 text-white">
{product.title}
</h2>
</motion.div>
);
};

View File

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

View File

@@ -0,0 +1,71 @@
"use client";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

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

View File

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

View File

@@ -0,0 +1,224 @@
"use client";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className,
)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View File

@@ -0,0 +1,126 @@
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"md:left-0 md:top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute md:left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-screen md:w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

Some files were not shown because too many files have changed in this diff Show More