feat(create-turbo): apply official-starter transform
36
apps/web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# 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*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
17
apps/web/.vscode/launch.json
vendored
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: Chrome",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm run dev",
|
||||
"serverReadyAction": {
|
||||
"pattern": "- Local:.+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
36
apps/web/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
BIN
apps/web/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/web/app/fonts/GeistMonoVF.woff
Normal file
BIN
apps/web/app/fonts/GeistVF.woff
Normal file
|
|
@ -1,13 +1,39 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "../components/theme/theme.css";
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,30 @@
|
|||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeProvider } from "@/components/theme/theme-provider";
|
||||
import { OpenAPI } from "@/lib/api/client";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
OpenAPI.BASE = "https://next-fast-turbo.vercel.app";
|
||||
}
|
||||
|
||||
console.log("Using OpenAPI.base", OpenAPI.BASE);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next-Fast-Turbo",
|
||||
description: "A Next.js, FastAPI and Turbo project scaffol",
|
||||
icons: {
|
||||
icon: ["/favicon.png"],
|
||||
},
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={cn(fontSans.variable, "bg-background font-sans")}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<DashboardLayout>{children}</DashboardLayout>
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
188
apps/web/app/page.module.css
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
.page {
|
||||
--gray-rgb: 0, 0, 0;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 20px 1fr 20px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
min-height: 100svh;
|
||||
padding: 80px;
|
||||
gap: 64px;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page {
|
||||
--gray-rgb: 255, 255, 255;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
font-family: var(--font-geist-mono);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.main li:not(:last-of-type) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.main code {
|
||||
font-family: inherit;
|
||||
background: var(--gray-alpha-100);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: none;
|
||||
font-family: var(--font-geist-sans);
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: none;
|
||||
font-family: var(--font-geist-sans);
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-family: var(--font-geist-sans);
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,99 @@
|
|||
import { CardsStats } from "./placeholder-stats";
|
||||
import SearchUsers from "@/components/search-users";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@repo/ui/button";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export default async function Page() {
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardsStats />
|
||||
<SearchUsers />
|
||||
<div className={styles.page}>
|
||||
<main className={styles.main}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol>
|
||||
<li>
|
||||
Get started by editing <code>app/page.tsx</code>
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<div className={styles.ctas}>
|
||||
<a
|
||||
className={styles.primary}
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.secondary}
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
<Button appName="web" className={styles.secondary}>
|
||||
Open alert
|
||||
</Button>
|
||||
</main>
|
||||
<footer className={styles.footer}>
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file-text.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
// Example cards from ShadCN: https://github.com/shadcn-ui/ui/tree/0fae3fd93ae749aca708bdfbbbeddc5d576bfb2e/apps/www/registry/default/example/cards
|
||||
import { FlexWrapper } from "@/components/flex-wrapper";
|
||||
import { DemoRevenue } from "@/components/demo-revenue";
|
||||
import { DemoSubscriptions } from "@/components/demo-subscriptions";
|
||||
import { DemoExercise } from "@/components/demo-exercise";
|
||||
import { DemoGoal } from "@/components/demo-goal";
|
||||
|
||||
export function CardsStats() {
|
||||
return (
|
||||
<FlexWrapper columns="4">
|
||||
<DemoRevenue />
|
||||
<DemoSubscriptions />
|
||||
<DemoExercise />
|
||||
<DemoGoal />
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function Page() {
|
||||
return <div>Settings page</div>;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
"use client";
|
||||
// Example data from ShadCN: https://github.com/shadcn-ui/ui/blob/0fae3fd93ae749aca708bdfbbbeddc5d576bfb2e/apps/www/registry/default/example/cards/stats.tsx#L61
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Line, LineChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||
import { twColourConfig } from "@/lib/twConfig";
|
||||
|
||||
const timeSeriesData = [
|
||||
{
|
||||
average: 400,
|
||||
today: 240,
|
||||
},
|
||||
{
|
||||
average: 300,
|
||||
today: 139,
|
||||
},
|
||||
{
|
||||
average: 200,
|
||||
today: 980,
|
||||
},
|
||||
{
|
||||
average: 278,
|
||||
today: 390,
|
||||
},
|
||||
{
|
||||
average: 189,
|
||||
today: 480,
|
||||
},
|
||||
{
|
||||
average: 239,
|
||||
today: 380,
|
||||
},
|
||||
{
|
||||
average: 349,
|
||||
today: 430,
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoExercise() {
|
||||
return (
|
||||
<Card className="w-full text-left">
|
||||
<CardHeader>
|
||||
<CardTitle>Exercise Minutes</CardTitle>
|
||||
<CardDescription>
|
||||
Your exercise minutes are ahead of where you normally are.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-4">
|
||||
{/* <div className="h-auto"> */}
|
||||
<div className="h-[140px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={timeSeriesData}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 10,
|
||||
left: 10,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
Average
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{payload[0].value}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
Today
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{payload[1].value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
strokeWidth={2}
|
||||
dataKey="average"
|
||||
activeDot={{
|
||||
r: 6,
|
||||
style: {
|
||||
fill: `${twColourConfig.primary.DEFAULT}`,
|
||||
opacity: 0.25,
|
||||
},
|
||||
}}
|
||||
stroke={twColourConfig.primary.DEFAULT}
|
||||
opacity={0.25}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="today"
|
||||
strokeWidth={2}
|
||||
activeDot={{
|
||||
r: 8,
|
||||
style: { fill: `${twColourConfig.primary.DEFAULT}` },
|
||||
}}
|
||||
stroke={twColourConfig.primary.DEFAULT}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
"use client";
|
||||
// https://github.com/shadcn-ui/ui/blob/0fae3fd93ae749aca708bdfbbbeddc5d576bfb2e/apps/www/registry/default/example/cards/activity-goal.tsx
|
||||
import * as React from "react";
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
import { Bar, BarChart, ResponsiveContainer } from "recharts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { twColourConfig } from "@/lib/twConfig";
|
||||
|
||||
const data = [
|
||||
{
|
||||
goal: 400,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 278,
|
||||
},
|
||||
{
|
||||
goal: 189,
|
||||
},
|
||||
{
|
||||
goal: 239,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 278,
|
||||
},
|
||||
{
|
||||
goal: 189,
|
||||
},
|
||||
{
|
||||
goal: 349,
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoGoal() {
|
||||
const [goal, setGoal] = React.useState(350);
|
||||
|
||||
function onClick(adjustment: number) {
|
||||
setGoal(Math.max(200, Math.min(400, goal + adjustment)));
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-base">Move Goal</CardTitle>
|
||||
<CardDescription>Set your daily activity goal.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-2">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 rounded-full"
|
||||
onClick={() => onClick(-10)}
|
||||
disabled={goal <= 200}
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
<span className="sr-only">Decrease</span>
|
||||
</Button>
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-5xl font-bold tracking-tighter">{goal}</div>
|
||||
<div className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
Calories/day
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 rounded-full"
|
||||
onClick={() => onClick(10)}
|
||||
disabled={goal >= 400}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">Increase</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="my-3 h-[60px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data}>
|
||||
<Bar
|
||||
dataKey="goal"
|
||||
style={{
|
||||
fill: `${twColourConfig.primary.DEFAULT}`,
|
||||
opacity: 0.2,
|
||||
}}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Set Goal</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"use client";
|
||||
// Example data from ShadCN: https://github.com/shadcn-ui/ui/blob/0fae3fd93ae749aca708bdfbbbeddc5d576bfb2e/apps/www/registry/default/example/cards/stats.tsx#L61
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Line, LineChart, ResponsiveContainer } from "recharts";
|
||||
import { twColourConfig } from "@/lib/twConfig";
|
||||
|
||||
const data = [
|
||||
{
|
||||
revenue: 10400,
|
||||
subscription: 240,
|
||||
},
|
||||
{
|
||||
revenue: 14405,
|
||||
subscription: 300,
|
||||
},
|
||||
{
|
||||
revenue: 9400,
|
||||
subscription: 200,
|
||||
},
|
||||
{
|
||||
revenue: 8200,
|
||||
subscription: 278,
|
||||
},
|
||||
{
|
||||
revenue: 7000,
|
||||
subscription: 189,
|
||||
},
|
||||
{
|
||||
revenue: 9600,
|
||||
subscription: 239,
|
||||
},
|
||||
{
|
||||
revenue: 11244,
|
||||
subscription: 278,
|
||||
},
|
||||
{
|
||||
revenue: 26475,
|
||||
subscription: 189,
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoRevenue() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row justify-between space-y-0 pb-4">
|
||||
<CardTitle className="text-base font-normal">Total Revenue</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-left">
|
||||
<div className="text-2xl font-bold">$15,231.89</div>
|
||||
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
|
||||
<div className="h-[140px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 10,
|
||||
left: 10,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<Line
|
||||
type="monotone"
|
||||
strokeWidth={2}
|
||||
dataKey="revenue"
|
||||
activeDot={{
|
||||
r: 6,
|
||||
style: {
|
||||
fill: `${twColourConfig.primary.DEFAULT}`,
|
||||
opacity: 0.25,
|
||||
},
|
||||
}}
|
||||
stroke={twColourConfig.primary.DEFAULT}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"use client";
|
||||
// Example data from ShadCN: https://github.com/shadcn-ui/ui/blob/0fae3fd93ae749aca708bdfbbbeddc5d576bfb2e/apps/www/registry/default/example/cards/stats.tsx#L61
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Bar, BarChart, ResponsiveContainer } from "recharts";
|
||||
import { twColourConfig } from "@/lib/twConfig";
|
||||
|
||||
const data = [
|
||||
{
|
||||
revenue: 10400,
|
||||
subscription: 240,
|
||||
},
|
||||
{
|
||||
revenue: 14405,
|
||||
subscription: 300,
|
||||
},
|
||||
{
|
||||
revenue: 9400,
|
||||
subscription: 200,
|
||||
},
|
||||
{
|
||||
revenue: 8200,
|
||||
subscription: 278,
|
||||
},
|
||||
{
|
||||
revenue: 7000,
|
||||
subscription: 189,
|
||||
},
|
||||
{
|
||||
revenue: 9600,
|
||||
subscription: 239,
|
||||
},
|
||||
{
|
||||
revenue: 11244,
|
||||
subscription: 278,
|
||||
},
|
||||
{
|
||||
revenue: 26475,
|
||||
subscription: 189,
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoSubscriptions() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-base font-normal">Subscriptions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-left">
|
||||
<div className="text-2xl font-bold">+2350</div>
|
||||
<p className="text-xs text-muted-foreground">+180.1% from last month</p>
|
||||
<div className="mt-4 h-[110px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data}>
|
||||
<Bar
|
||||
dataKey="subscription"
|
||||
fill={twColourConfig.primary.DEFAULT}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { ReactNode, Children } from "react";
|
||||
|
||||
const flexVariants = cva("", {
|
||||
variants: {
|
||||
columns: {
|
||||
default: "",
|
||||
"1": "flex-1 basis-full",
|
||||
"2": "flex-1 basis-full sm:basis-[48%]",
|
||||
"3": "flex-1 basis-full sm:basis-[48%] md:basis-[32%]",
|
||||
"4": "flex-1 basis-full sm:basis-[48%] md:basis-[32%] lg:basis-[24%]",
|
||||
"5": "flex-1 basis-full sm:basis-[48%] md:basis-[32%] lg:basis-[19%]",
|
||||
"6": "flex-1 basis-full sm:basis-[48%] md:basis-[32%] lg:basis-[19%] xl:basis-[12%]",
|
||||
},
|
||||
horizontal_position: {
|
||||
start: "justify-start",
|
||||
center: "justify-center text-center",
|
||||
end: "justify-end",
|
||||
none: null,
|
||||
},
|
||||
vertical_position: {
|
||||
start: "items-start",
|
||||
center: "items-center text-center",
|
||||
end: "items-end",
|
||||
none: null,
|
||||
},
|
||||
borders: {
|
||||
default: "border rounded-md p-5",
|
||||
none: null,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
columns: "default",
|
||||
horizontal_position: "none",
|
||||
vertical_position: "none",
|
||||
borders: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export type FlexWrapperProps = VariantProps<typeof flexVariants> & {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function FlexWrapper({
|
||||
children,
|
||||
className,
|
||||
columns,
|
||||
horizontal_position,
|
||||
vertical_position,
|
||||
}: FlexWrapperProps) {
|
||||
return (
|
||||
<div
|
||||
id="flex-wrapper"
|
||||
className={cn(
|
||||
// isCentered && "place-items-stretch",
|
||||
flexVariants({ horizontal_position, vertical_position }),
|
||||
"w-full flex flex-wrap gap-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{Children.map(children, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(flexVariants({ columns }), "flex min-h-full")}
|
||||
>
|
||||
{/* <div key={index} className={cn("flex min-h-full")}> */}
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { ReactNode, Children } from "react";
|
||||
|
||||
const gridVariants = cva("w-full grid gap-4 justify-between", {
|
||||
variants: {
|
||||
columns: {
|
||||
default:
|
||||
"grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6",
|
||||
"1": "grid-cols-1",
|
||||
"2": "grid-cols-1 sm:grid-cols-2",
|
||||
"3": "grid-cols-1 sm:grid-cols-2 md:grid-cols-3",
|
||||
"4": "grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
||||
"5": "grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5",
|
||||
"6": "grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6",
|
||||
},
|
||||
horizontal_position: {
|
||||
start: "justify-start",
|
||||
center: "justify-center text-center",
|
||||
end: "justify-end",
|
||||
},
|
||||
vertical_position: {
|
||||
start: "items-start",
|
||||
center: "items-center text-center",
|
||||
end: "items-end",
|
||||
},
|
||||
borders: {
|
||||
default: "border rounded-md p-5",
|
||||
none: null,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
columns: "default",
|
||||
horizontal_position: "center",
|
||||
vertical_position: "center",
|
||||
borders: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export type GridWrapperProps = VariantProps<typeof gridVariants> & {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function GridWrapper({
|
||||
children,
|
||||
className,
|
||||
columns,
|
||||
horizontal_position,
|
||||
vertical_position,
|
||||
}: GridWrapperProps) {
|
||||
const isCentered =
|
||||
horizontal_position === "center" && vertical_position === "center";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isCentered && "place-items-stretch",
|
||||
gridVariants({ columns, horizontal_position, vertical_position }),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{Children.map(children, (child, index) => (
|
||||
<div key={index} className={cn("flex min-h-full")}>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
// https://lucide.dev/icons/
|
||||
// Country icons: https://www.svgrepo.com/collection/countrys-flags/
|
||||
// LinkedIn/Facebook/Twitter: https://icons8.com/icons/
|
||||
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
BadgePercent,
|
||||
BarChart,
|
||||
Bell,
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
ClipboardList,
|
||||
Copy,
|
||||
File,
|
||||
Gauge,
|
||||
Globe,
|
||||
Home,
|
||||
LayoutTemplate,
|
||||
Link,
|
||||
LucideProps,
|
||||
Menu,
|
||||
Moon,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Plus,
|
||||
PoundSterling,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
SunMedium,
|
||||
User2,
|
||||
Users,
|
||||
Workflow,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
export const Icons = {
|
||||
activity: Activity,
|
||||
analytics: BarChart,
|
||||
arrowRight: ArrowRight,
|
||||
badgePercent: BadgePercent,
|
||||
building: Building2,
|
||||
chevronDown: ChevronDown,
|
||||
chevronUpDown: ChevronsUpDown,
|
||||
collaboration: Users,
|
||||
copy: Copy,
|
||||
file: File,
|
||||
globe: Globe,
|
||||
home: Home,
|
||||
link: Link,
|
||||
menu: Menu,
|
||||
menuClose: X,
|
||||
moon: Moon,
|
||||
notification: Bell,
|
||||
panelLeftClose: PanelLeftClose,
|
||||
panelLeftOpen: PanelLeftOpen,
|
||||
performance: Gauge,
|
||||
plus: Plus,
|
||||
poundSterling: PoundSterling,
|
||||
rules: ClipboardList,
|
||||
settings: Settings,
|
||||
slider: SlidersHorizontal,
|
||||
sun: SunMedium,
|
||||
template: LayoutTemplate,
|
||||
user2: User2,
|
||||
workflow: Workflow,
|
||||
github: (props: LucideProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 96" {...props}>
|
||||
<path d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" />
|
||||
</svg>
|
||||
),
|
||||
linkedin: (props: LucideProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" {...props}>
|
||||
<path d="M41,4H9C6.24,4,4,6.24,4,9v32c0,2.76,2.24,5,5,5h32c2.76,0,5-2.24,5-5V9C46,6.24,43.76,4,41,4z M17,20v19h-6V20H17z M11,14.47c0-1.4,1.2-2.47,3-2.47s2.93,1.07,3,2.47c0,1.4-1.12,2.53-3,2.53C12.2,17,11,15.87,11,14.47z M39,39h-6c0,0,0-9.26,0-10 c0-2-1-4-3.5-4.04h-0.08C27,24.96,26,27.02,26,29c0,0.91,0,10,0,10h-6V20h6v2.56c0,0,1.93-2.56,5.81-2.56 c3.97,0,7.19,2.73,7.19,8.26V39z" />
|
||||
</svg>
|
||||
),
|
||||
doubleChevron: (props: LucideProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className="text-foreground lucide lucide-chevrons-up-down"
|
||||
>
|
||||
<path d="m7 15 5 5 5-5" />
|
||||
<path d="m7 9 5-5 5 5" />
|
||||
</svg>
|
||||
),
|
||||
logo: (props: LucideProps) => (
|
||||
<svg
|
||||
id="Layer_1"
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
{`.cls-1{fill:hsl(var(--foreground));}.cls-2{fill:none}.cls-3{stroke:hsl(var(--background));}.cls-4{stroke-width:8px;}.cls-5{stroke-linecap:round;}.cls-6{stroke-linejoin:round;}`}
|
||||
</style>
|
||||
</defs>
|
||||
<circle className="cls-1" cx="50" cy="50" r="49.9" />
|
||||
<path
|
||||
className="cls-2 cls-3 cls-4 cls-5 cls-6"
|
||||
d="M61.77,69.61,81.38,50,61.77,30.39"
|
||||
/>
|
||||
<path
|
||||
className="cls-2 cls-3 cls-4 cls-5 cls-6"
|
||||
d="M38.23,69.61,18.62,50,38.23,30.39"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
.dashboardWrapper {
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-areas: "sidebar main";
|
||||
--header-height: 5rem;
|
||||
--footer-height: 2rem;
|
||||
--small-spacing: 1rem; /* tw class: gap-4, m-4 etc. */
|
||||
--large-spacing: 2rem; /* tw class: gap-8, m-8 etc. */
|
||||
--header-size: calc(var(--header-height) + var(--small-spacing));
|
||||
--footer-size: calc(var(--footer-height) + var(--small-spacing));
|
||||
}
|
||||
|
||||
.dashboardSidebar {
|
||||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
.dashboardMain {
|
||||
@apply gap-[var(--large-spacing)];
|
||||
grid-area: main;
|
||||
grid-template-rows: var(--header-height) calc(
|
||||
100vh - (var(--header-height) + var(--footer-height))
|
||||
);
|
||||
grid-template-areas: "header" "content";
|
||||
}
|
||||
|
||||
.dashboardHeader {
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.dashboardContent {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.dashboardContentWrapper {
|
||||
@apply gap-[var(--small-spacing)];
|
||||
min-height: calc(
|
||||
100vh -
|
||||
(
|
||||
var(--header-size) + var(--footer-size) + var(--small-spacing) +
|
||||
var(--small-spacing)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import styles from "./DashboardLayout.module.css";
|
||||
import { Footer, Header, Sidebar } from "./layout-components";
|
||||
|
||||
type DashboardLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
return (
|
||||
<div
|
||||
className={`grid h-screen text-muted-foreground ${styles.dashboardWrapper}`}
|
||||
>
|
||||
<div className={`hidden h-screen sm:block ${styles.dashboardSidebar}`}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className={`grid overflow-auto ${styles.dashboardMain}`}>
|
||||
<div
|
||||
className={`fixed w-screen z-50 top-0 flex h-20 items-center border-b border-border bg-background/30 px-8 backdrop-blur ${styles.dashboardHeader}`}
|
||||
>
|
||||
<Header />
|
||||
</div>
|
||||
<div className={styles.dashboardContent}>
|
||||
<div
|
||||
className={`flex flex-col px-8 ${styles.dashboardContentWrapper}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="px-8">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as DashboardLayout } from "../dashboard/DashboardLayout";
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/* Vars here are coming from /DashboardLayout.module.css */
|
||||
.dashboardFooter {
|
||||
@apply mt-[var(--small-spacing)] min-h-[var(--footer-height)];
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import styles from "./Footer.module.css";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className={`w-full text-sm ${styles.dashboardFooter}`}>
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 justify-end">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Copyright© Next-Fast-Turbo. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./Footer";
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { navConfig } from "@/lib/config";
|
||||
import { ModeToggle } from "@/components/theme/mode-toggle";
|
||||
import { SidebarMobile } from "../../layout-components";
|
||||
|
||||
const Header = () => {
|
||||
const pathName = usePathname();
|
||||
const pageTitle = navConfig.navLinks.find((elem) => {
|
||||
if (elem.href === pathName) {
|
||||
return elem.pageTitle;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-row items-center justify-between text-foreground">
|
||||
<div className="block w-full font-medium sm:block">
|
||||
{pageTitle?.pageTitle}
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-end gap-4">
|
||||
<div className="block sm:hidden">
|
||||
<SidebarMobile />
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./Header";
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function LinkComponent(...props: any) {
|
||||
const activeLink = props[0].activeLink;
|
||||
const collapsed = props[0].collapsed;
|
||||
const animationDuration = props[0].animationDuration;
|
||||
|
||||
return (
|
||||
<Link href={props[0].href}>
|
||||
{collapsed ? (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`flex flex-row gap-6 rounded-md py-3 font-normal ${
|
||||
collapsed ? "justify-center" : "items-center"
|
||||
} ${activeLink ? "bg-border text-foreground" : ""}`}
|
||||
>
|
||||
<div className={collapsed ? "p-0" : "pl-4"}>
|
||||
{props[0].icon}
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
layout
|
||||
animate={{
|
||||
x: collapsed ? -20 : 0,
|
||||
y: collapsed ? 0 : 0,
|
||||
opacity: collapsed ? 0 : 1,
|
||||
width: collapsed ? 0 : "auto",
|
||||
display: collapsed ? "none" : "block",
|
||||
}}
|
||||
transition={{ duration: animationDuration }}
|
||||
className="text-sm"
|
||||
>
|
||||
{props[0].label}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{props[0].label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div
|
||||
className={`flex flex-row gap-6 rounded-md py-3 font-normal ${
|
||||
collapsed ? "justify-center" : "items-center"
|
||||
} ${activeLink ? "bg-border text-foreground" : ""}`}
|
||||
>
|
||||
<div className={collapsed ? "p-0" : "pl-4"}>{props[0].icon}</div>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
layout
|
||||
animate={{
|
||||
x: collapsed ? -20 : 0,
|
||||
y: collapsed ? 0 : 0,
|
||||
opacity: collapsed ? 0 : 1,
|
||||
width: collapsed ? 0 : "auto",
|
||||
display: collapsed ? "none" : "block",
|
||||
}}
|
||||
transition={{ duration: animationDuration }}
|
||||
className="text-sm"
|
||||
>
|
||||
{props[0].label}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import LinkComponent from "./NavLink";
|
||||
import { navConfig } from "@/lib/config/";
|
||||
|
||||
type NavLinksProps = {
|
||||
collapsed: boolean;
|
||||
animationDuration: number;
|
||||
};
|
||||
|
||||
const NavLinks: FC<NavLinksProps> = ({ collapsed, animationDuration }) => {
|
||||
const pathName = usePathname();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div id="topNavLinks">
|
||||
{navConfig.navLinks.map((link, index) => {
|
||||
if (link.navLocation === "top") {
|
||||
const activeLink = pathName === link.href;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="px-5 text-muted-foreground transition-all duration-300 hover:text-foreground"
|
||||
>
|
||||
<LinkComponent
|
||||
activeLink={activeLink}
|
||||
href={link.href}
|
||||
label={link.label}
|
||||
icon={link.icon}
|
||||
animationDuration={animationDuration}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div id="btmNavLinks">
|
||||
{navConfig.navLinks.map((link, index) => {
|
||||
if (link.navLocation === "bottom") {
|
||||
const activeLink = pathName === link.href;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="px-5 text-muted-foreground transition-all duration-300 hover:text-foreground"
|
||||
>
|
||||
<LinkComponent
|
||||
activeLink={activeLink}
|
||||
href={link.href}
|
||||
label={link.label}
|
||||
icon={link.icon}
|
||||
animationDuration={animationDuration}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavLinks;
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { motion } from "framer-motion";
|
||||
import NavLinks from "./NavLinks";
|
||||
|
||||
const Sidebar = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const animationDuration = 0.4;
|
||||
const sideBarWidth = "250px";
|
||||
|
||||
// Load collapsed state from localStorage on component mount
|
||||
useEffect(() => {
|
||||
const collapsedState = localStorage.getItem("sidebarCollapsed");
|
||||
if (collapsedState !== null) {
|
||||
setCollapsed(collapsedState === "true" ? true : false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
localStorage.setItem("sidebarCollapsed", (!collapsed).toString());
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ width: collapsed ? "88px" : sideBarWidth }}
|
||||
animate={{
|
||||
minWidth: collapsed ? "88px" : sideBarWidth,
|
||||
width: collapsed ? "88px" : sideBarWidth,
|
||||
}}
|
||||
transition={{ duration: animationDuration }}
|
||||
id="sidebar"
|
||||
className={`flex h-full flex-col justify-between gap-8 border-r border-border`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-20 items-center justify-between border-b border-border ${
|
||||
collapsed ? "px-8" : "px-4"
|
||||
} `}
|
||||
>
|
||||
<Link href="/">
|
||||
<motion.div
|
||||
layout
|
||||
animate={{
|
||||
x: collapsed ? -100 : 0,
|
||||
y: collapsed ? 0 : 0,
|
||||
opacity: collapsed ? 0 : 1,
|
||||
width: collapsed ? 0 : "auto",
|
||||
display: collapsed ? "none" : "block",
|
||||
}}
|
||||
transition={{ duration: animationDuration }}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1 font-semibold text-sm text-foreground">
|
||||
<span>
|
||||
<Icons.logo className="h-5" />
|
||||
</span>
|
||||
Next-Fast-Turbo
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{collapsed ? (
|
||||
<Icons.panelLeftOpen
|
||||
className="h-6 w-6 cursor-pointer text-muted-foreground transition-all hover:text-foreground hover:duration-300"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
) : (
|
||||
<Icons.panelLeftClose
|
||||
className="h-6 w-6 cursor-pointer text-muted-foreground transition-all hover:text-foreground hover:duration-300"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 border-border pb-8">
|
||||
<NavLinks collapsed={collapsed} animationDuration={animationDuration} />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
"use client";
|
||||
import { useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { Squash as Hamburger } from "hamburger-react";
|
||||
import { useClickAway } from "react-use";
|
||||
import { navConfig } from "@/lib/config/";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const SidebarMobile = () => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useClickAway(ref, () => setOpen(false));
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Hamburger toggled={isOpen} size={20} toggle={setOpen} />
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed left-0 w-[85%] py-12 h-screen px-8 bg-background"
|
||||
>
|
||||
<ul className="grid min-h-72 content-evenly">
|
||||
{navConfig.navLinks.map((link, index) => {
|
||||
return (
|
||||
<motion.li
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
delay: 0.1 + index / 10,
|
||||
}}
|
||||
key={link.pageTitle}
|
||||
className="w-full"
|
||||
>
|
||||
<Link href={link.href}>{link.pageTitle}</Link>
|
||||
<Separator className="my-2" />
|
||||
</motion.li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarMobile;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Sidebar } from "./Sidebar";
|
||||
export { default as SidebarMobile } from "./SidebarMobile";
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export { Sidebar } from "./Sidebar";
|
||||
export { SidebarMobile } from "./Sidebar";
|
||||
export { default as Header } from "./Header";
|
||||
export { default as Footer } from "./Footer";
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
"use client";
|
||||
import * as React from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UsersService, UserSearchResults } from "@/lib/api/client";
|
||||
|
||||
const searchOnFields = ["id", "email", "forename", "surname"];
|
||||
|
||||
const FormSchema = z.object({
|
||||
keyword: z.string().optional(),
|
||||
searchOn: z.enum(["id", "email", "forename", "surname"]).optional(),
|
||||
searchResults: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Must return at least 1 result",
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export default function SearchUsers() {
|
||||
const [searchResults, setSearchResults] = React.useState<UserSearchResults>({
|
||||
results: [],
|
||||
});
|
||||
const [error, setError] = React.useState(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
keyword: ".com",
|
||||
searchOn: "email",
|
||||
searchResults: "10",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
console.log(data);
|
||||
try {
|
||||
setLoading(true); // set loading state
|
||||
setError(null); // clear error state if it exists
|
||||
const maxResults = data.searchResults ? parseInt(data.searchResults) : 10;
|
||||
const response = await UsersService.usersSearchUsers({
|
||||
keyword: data.keyword,
|
||||
searchOn: data.searchOn,
|
||||
maxResults: maxResults,
|
||||
});
|
||||
setLoading(false);
|
||||
console.log(response);
|
||||
setSearchResults(response);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.log("Error received", error);
|
||||
setLoading(false);
|
||||
setSearchResults({ results: [] });
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<Card>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<CardHeader>
|
||||
<CardTitle>FastAPI data</CardTitle>
|
||||
<CardDescription>
|
||||
Data coming from FastAPI backend
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keyword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full md:w-1/3">
|
||||
<FormLabel>Keyword</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-foreground bg-none w-full"
|
||||
placeholder="shadcn"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>The keyword to search.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="searchResults"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full md:w-1/3">
|
||||
<FormLabel>Max results</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-foreground bg-none w-full"
|
||||
min={1}
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set maximum number of results to return.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="searchOn"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full md:w-1/3">
|
||||
<FormLabel>Search field</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
defaultValue="email"
|
||||
onValueChange={field.onChange}
|
||||
className="grid grid-cols-2 gap-x-8 w-full text-foreground"
|
||||
>
|
||||
{searchOnFields.map((item) => (
|
||||
<FormItem
|
||||
key={item}
|
||||
className="flex space-x-1 space-y-0 "
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={item} />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{item.charAt(0).toUpperCase() + item.slice(1)}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormDescription>The field to search on.</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex flex-row gap-4 w-full my-4">
|
||||
<Button className="min-w-24" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-24"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setError(null); // clear error state
|
||||
}}
|
||||
>
|
||||
Reset form
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</Form>
|
||||
|
||||
{loading ? (
|
||||
<Skeleton className="bg-foreground/10 text-foreground p-5 rounded-lg" />
|
||||
) : null}
|
||||
|
||||
{searchResults.results.length >= 1 ? (
|
||||
// Render the results if searchResults is set
|
||||
<div className="bg-foreground/10 text-foreground p-5 rounded-lg max-h-80 overflow-y-auto">
|
||||
{/* Replace this with your code to render the results */}
|
||||
<pre>{JSON.stringify(searchResults, null, 2)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* This can be handled better to understand what type of error is occurring rather than just a blanket handler */}
|
||||
{error ? (
|
||||
<div>
|
||||
Couldn't find any results that match your criteria. Please try again.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// https://github.com/shadcn-ui/taxonomy/blob/main/components/tailwind-indicator.tsx#L1
|
||||
|
||||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === "production") return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-5 left-5 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-red-800 p-4 font-mono text-sm font-semibold border border-white text-white">
|
||||
<div className="block sm:hidden">xs</div>
|
||||
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
|
||||
sm
|
||||
</div>
|
||||
<div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
|
||||
<div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
|
||||
<div className="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
const handleSubmit = () => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleSubmit} variant="outline" size="icon">
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<MoonIcon 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 263.4 70% 50.4%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// WIP component. Will flesh out more as we develop
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const typographyVariants = cva("m-0 self-center p-0", {
|
||||
variants: {
|
||||
variant: {
|
||||
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
|
||||
h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
|
||||
h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
|
||||
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
|
||||
p: "leading-7 [&:not(:first-child)]:mt-6",
|
||||
code: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
|
||||
lead: "text-xl text-muted-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "text-lg",
|
||||
sm: "text-sm",
|
||||
lg: "text-xl",
|
||||
},
|
||||
colour: {
|
||||
default: "text-foreground",
|
||||
muted: "text-muted-foreground",
|
||||
accent: "text-accent",
|
||||
inverted: "text-background",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "p",
|
||||
size: "default",
|
||||
colour: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export interface TypographyProps
|
||||
extends React.HTMLAttributes<HTMLHeadingElement>,
|
||||
VariantProps<typeof typographyVariants> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Typography = ({
|
||||
variant,
|
||||
size,
|
||||
colour,
|
||||
className,
|
||||
children,
|
||||
}: TypographyProps) => {
|
||||
let HeadingComponent: React.ElementType = "div";
|
||||
|
||||
switch (variant) {
|
||||
case "h1":
|
||||
HeadingComponent = "h1";
|
||||
break;
|
||||
case "h2":
|
||||
HeadingComponent = "h2";
|
||||
break;
|
||||
case "h3":
|
||||
HeadingComponent = "h3";
|
||||
break;
|
||||
case "h4":
|
||||
HeadingComponent = "h4";
|
||||
break;
|
||||
case "p":
|
||||
HeadingComponent = "p";
|
||||
break;
|
||||
case "code":
|
||||
HeadingComponent = "code";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadingComponent
|
||||
className={cn(typographyVariants({ variant, size, className, colour }))}
|
||||
>
|
||||
{children}
|
||||
</HeadingComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export { Typography, typographyVariants as buttonVariants };
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
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",
|
||||
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 };
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-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,
|
||||
};
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
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,
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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-background 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 };
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
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 };
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[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}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import type { ApiResult } from "./ApiResult";
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: any;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(
|
||||
request: ApiRequestOptions,
|
||||
response: ApiResult,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
|
||||
this.name = "ApiError";
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiRequestOptions = {
|
||||
readonly method:
|
||||
| "GET"
|
||||
| "PUT"
|
||||
| "POST"
|
||||
| "DELETE"
|
||||
| "OPTIONS"
|
||||
| "HEAD"
|
||||
| "PATCH";
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, any>;
|
||||
readonly cookies?: Record<string, any>;
|
||||
readonly headers?: Record<string, any>;
|
||||
readonly query?: Record<string, any>;
|
||||
readonly formData?: Record<string, any>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly errors?: Record<number, string>;
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiResult = {
|
||||
readonly url: string;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly body: any;
|
||||
};
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export class CancelError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CancelError";
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean;
|
||||
readonly isRejected: boolean;
|
||||
readonly isCancelled: boolean;
|
||||
|
||||
(cancelHandler: () => void): void;
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
#isResolved: boolean;
|
||||
#isRejected: boolean;
|
||||
#isCancelled: boolean;
|
||||
readonly #cancelHandlers: (() => void)[];
|
||||
readonly #promise: Promise<T>;
|
||||
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||
#reject?: (reason?: any) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
onCancel: OnCancel,
|
||||
) => void,
|
||||
) {
|
||||
this.#isResolved = false;
|
||||
this.#isRejected = false;
|
||||
this.#isCancelled = false;
|
||||
this.#cancelHandlers = [];
|
||||
this.#promise = new Promise<T>((resolve, reject) => {
|
||||
this.#resolve = resolve;
|
||||
this.#reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isResolved = true;
|
||||
if (this.#resolve) this.#resolve(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: any): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isRejected = true;
|
||||
if (this.#reject) this.#reject(reason);
|
||||
};
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, "isResolved", {
|
||||
get: (): boolean => this.#isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, "isRejected", {
|
||||
get: (): boolean => this.#isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, "isCancelled", {
|
||||
get: (): boolean => this.#isCancelled,
|
||||
});
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||
});
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Cancellable Promise";
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.#promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
|
||||
): Promise<T | TResult> {
|
||||
return this.#promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this.#promise.finally(onFinally);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isCancelled = true;
|
||||
if (this.#cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this.#cancelHandlers) {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Cancellation threw an error", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#cancelHandlers.length = 0;
|
||||
if (this.#reject) this.#reject(new CancelError("Request aborted"));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this.#isCancelled;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: "https://next-fast-turbo-api.vercel.app",
|
||||
VERSION: "0.1.0",
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: "include",
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
||||
|
|
@ -1,367 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import axios from "axios";
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosInstance,
|
||||
} from "axios";
|
||||
import FormData from "form-data";
|
||||
|
||||
import { ApiError } from "./ApiError";
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import type { ApiResult } from "./ApiResult";
|
||||
import { CancelablePromise } from "./CancelablePromise";
|
||||
import type { OnCancel } from "./CancelablePromise";
|
||||
import type { OpenAPIConfig } from "./OpenAPI";
|
||||
|
||||
export const isDefined = <T>(
|
||||
value: T | null | undefined,
|
||||
): value is Exclude<T, null | undefined> => {
|
||||
return value !== undefined && value !== null;
|
||||
};
|
||||
|
||||
export const isString = (value: any): value is string => {
|
||||
return typeof value === "string";
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: any): value is string => {
|
||||
return isString(value) && value !== "";
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
typeof value.type === "string" &&
|
||||
typeof value.stream === "function" &&
|
||||
typeof value.arrayBuffer === "function" &&
|
||||
typeof value.constructor === "function" &&
|
||||
typeof value.constructor.name === "string" &&
|
||||
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||
);
|
||||
};
|
||||
|
||||
export const isFormData = (value: any): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
export const isSuccess = (status: number): boolean => {
|
||||
return status >= 200 && status < 300;
|
||||
};
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString("base64");
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueryString = (params: Record<string, any>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: any) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => {
|
||||
process(key, v);
|
||||
});
|
||||
} else if (typeof value === "object") {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
});
|
||||
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join("&")}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace("{api-version}", config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = `${config.BASE}${path}`;
|
||||
if (options.query) {
|
||||
return `${url}${getQueryString(options.query)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getFormData = (
|
||||
options: ApiRequestOptions,
|
||||
): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(
|
||||
options: ApiRequestOptions,
|
||||
resolver?: T | Resolver<T>,
|
||||
): Promise<T | undefined> => {
|
||||
if (typeof resolver === "function") {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async (
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
formData?: FormData,
|
||||
): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
resolve(options, config.TOKEN),
|
||||
resolve(options, config.USERNAME),
|
||||
resolve(options, config.PASSWORD),
|
||||
resolve(options, config.HEADERS),
|
||||
]);
|
||||
|
||||
const formHeaders =
|
||||
(typeof formData?.getHeaders === "function" && formData?.getHeaders()) ||
|
||||
{};
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: "application/json",
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
...formHeaders,
|
||||
})
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.reduce(
|
||||
(headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}),
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers["Authorization"] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.mediaType) {
|
||||
headers["Content-Type"] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers["Content-Type"] = options.body.type || "application/octet-stream";
|
||||
} else if (isString(options.body)) {
|
||||
headers["Content-Type"] = "text/plain";
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||
if (options.body) {
|
||||
return options.body;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
url: string,
|
||||
body: any,
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance,
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const source = axios.CancelToken.source();
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
url,
|
||||
headers,
|
||||
data: body ?? formData,
|
||||
method: options.method,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
cancelToken: source.token,
|
||||
};
|
||||
|
||||
onCancel(() => source.cancel("The user aborted a request."));
|
||||
|
||||
try {
|
||||
return await axiosClient.request(requestConfig);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<T>;
|
||||
if (axiosError.response) {
|
||||
return axiosError.response;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResponseHeader = (
|
||||
response: AxiosResponse<any>,
|
||||
responseHeader?: string,
|
||||
): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader];
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = (response: AxiosResponse<any>): any => {
|
||||
if (response.status !== 204) {
|
||||
return response.data;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (
|
||||
options: ApiRequestOptions,
|
||||
result: ApiResult,
|
||||
): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
...options.errors,
|
||||
};
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? "unknown";
|
||||
const errorStatusText = result.statusText ?? "unknown";
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(
|
||||
options,
|
||||
result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @param axiosClient The axios client instance to use
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
const headers = await getHeaders(config, options, formData);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
const response = await sendRequest<T>(
|
||||
config,
|
||||
options,
|
||||
url,
|
||||
body,
|
||||
formData,
|
||||
headers,
|
||||
onCancel,
|
||||
axiosClient,
|
||||
);
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(
|
||||
response,
|
||||
options.responseHeader,
|
||||
);
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: isSuccess(response.status),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? responseBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from "./core/ApiError";
|
||||
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
|
||||
export { OpenAPI } from "./core/OpenAPI";
|
||||
export type { OpenAPIConfig } from "./core/OpenAPI";
|
||||
|
||||
export type { HTTPValidationError } from "./models/HTTPValidationError";
|
||||
export type { Spell } from "./models/Spell";
|
||||
export type { SpellSearchResults } from "./models/SpellSearchResults";
|
||||
export type { User } from "./models/User";
|
||||
export type { UserCreate } from "./models/UserCreate";
|
||||
export type { UserSearchResults } from "./models/UserSearchResults";
|
||||
export type { ValidationError } from "./models/ValidationError";
|
||||
|
||||
export { SpellsService } from "./services/SpellsService";
|
||||
export { UsersService } from "./services/UsersService";
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ValidationError } from "./ValidationError";
|
||||
export type HTTPValidationError = {
|
||||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Spell = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { Spell } from "./Spell";
|
||||
export type SpellSearchResults = {
|
||||
results: Array<Spell>;
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type User = {
|
||||
id: string;
|
||||
forename: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UserCreate = {
|
||||
forename: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { User } from "./User";
|
||||
export type UserSearchResults = {
|
||||
results: Array<User>;
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ValidationError = {
|
||||
loc: Array<string | number>;
|
||||
msg: string;
|
||||
type: string;
|
||||
};
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { Spell } from "../models/Spell";
|
||||
import type { SpellSearchResults } from "../models/SpellSearchResults";
|
||||
import type { CancelablePromise } from "../core/CancelablePromise";
|
||||
import { OpenAPI } from "../core/OpenAPI";
|
||||
import { request as __request } from "../core/request";
|
||||
export class SpellsService {
|
||||
/**
|
||||
* Get Spell
|
||||
* Returns a spell from a spell_id.
|
||||
*
|
||||
* **Returns:**
|
||||
* - spell: spell object.
|
||||
* @returns Spell Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static spellsGetSpell({
|
||||
spellId,
|
||||
}: {
|
||||
spellId: string;
|
||||
}): CancelablePromise<Spell> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/spells/get/",
|
||||
query: {
|
||||
spell_id: spellId,
|
||||
},
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get All Spells
|
||||
* Returns a list of all spells.
|
||||
*
|
||||
* **Returns:**
|
||||
* - list[spell]: List of all spells.
|
||||
* @returns Spell Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static spellsGetAllSpells(): CancelablePromise<Array<Spell>> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/spells/get-all/",
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Search Spells
|
||||
* Search for spells based on a keyword and return the top `max_results` items.
|
||||
*
|
||||
* **Args:**
|
||||
* - keyword (str, optional): The keyword to search for. Defaults to None.
|
||||
* - max_results (int, optional): The maximum number of search results to return. Defaults to 10.
|
||||
* - search_on (str, optional): The field to perform the search on. Defaults to "email".
|
||||
*
|
||||
* **Returns:**
|
||||
* - spellSearchResults: Object containing a list of the top `max_results` items that match the keyword.
|
||||
* @returns SpellSearchResults Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static spellsSearchSpells({
|
||||
searchOn = "spells",
|
||||
keyword,
|
||||
maxResults,
|
||||
}: {
|
||||
searchOn?: "id" | "spells" | "description";
|
||||
keyword?: string | number | null;
|
||||
maxResults?: number | null;
|
||||
}): CancelablePromise<SpellSearchResults> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/spells/search/",
|
||||
query: {
|
||||
search_on: searchOn,
|
||||
keyword: keyword,
|
||||
max_results: maxResults,
|
||||
},
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { User } from "../models/User";
|
||||
import type { UserCreate } from "../models/UserCreate";
|
||||
import type { UserSearchResults } from "../models/UserSearchResults";
|
||||
import type { CancelablePromise } from "../core/CancelablePromise";
|
||||
import { OpenAPI } from "../core/OpenAPI";
|
||||
import { request as __request } from "../core/request";
|
||||
export class UsersService {
|
||||
/**
|
||||
* Get User
|
||||
* Returns a user from a user_id.
|
||||
*
|
||||
* **Returns:**
|
||||
* - User: User object.
|
||||
* @returns User Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static usersGetUser({
|
||||
userId,
|
||||
}: {
|
||||
userId: string;
|
||||
}): CancelablePromise<User> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/users/get/",
|
||||
query: {
|
||||
user_id: userId,
|
||||
},
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get All Users
|
||||
* Returns a list of all users.
|
||||
*
|
||||
* **Returns:**
|
||||
* - list[User]: List of all users.
|
||||
* @returns User Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static usersGetAllUsers(): CancelablePromise<Array<User>> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/users/get-all/",
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Search Users
|
||||
* Search for users based on a keyword and return the top `max_results` items.
|
||||
*
|
||||
* **Args:**
|
||||
* - keyword (str, optional): The keyword to search for. Defaults to None.
|
||||
* - max_results (int, optional): The maximum number of search results to return. Defaults to 10.
|
||||
* - search_on (str, optional): The field to perform the search on. Defaults to "email".
|
||||
*
|
||||
* **Returns:**
|
||||
* - UserSearchResults: Object containing a list of the top `max_results` items that match the keyword.
|
||||
* @returns UserSearchResults Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static usersSearchUsers({
|
||||
searchOn = "email",
|
||||
keyword,
|
||||
maxResults,
|
||||
}: {
|
||||
searchOn?: "id" | "email" | "forename" | "surname";
|
||||
keyword?: string | number | null;
|
||||
maxResults?: number | null;
|
||||
}): CancelablePromise<UserSearchResults> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/users/search/",
|
||||
query: {
|
||||
search_on: searchOn,
|
||||
keyword: keyword,
|
||||
max_results: maxResults,
|
||||
},
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create User
|
||||
* Craete a new user.
|
||||
*
|
||||
* **Args:**
|
||||
* - user_in (UserCreate): JSON of the user to create. Forename, surname and email. Email must be unique.
|
||||
*
|
||||
* **Returns:**
|
||||
* - User: User object
|
||||
* @returns User Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static usersCreateUser({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UserCreate;
|
||||
}): CancelablePromise<User> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/api/v1/users/create",
|
||||
body: requestBody,
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
404: `Not found`,
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./nav.tsx";
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { Icons } from "@/components/icons";
|
||||
|
||||
export type NavConfig = typeof navConfig;
|
||||
|
||||
export const navConfig = {
|
||||
navLinks: [
|
||||
{
|
||||
icon: <Icons.home className="h-5 w-5" />,
|
||||
iconMobile: <Icons.home className="h-5 w-5" />,
|
||||
label: "Overview",
|
||||
href: "/",
|
||||
pageTitle: "Overview",
|
||||
navLocation: "top",
|
||||
},
|
||||
{
|
||||
icon: <Icons.settings className="h-5 w-5" />,
|
||||
iconMobile: <Icons.settings className="h-5 w-5" />,
|
||||
label: "Settings",
|
||||
href: "/settings/",
|
||||
pageTitle: "Account settings",
|
||||
navLocation: "bottom",
|
||||
},
|
||||
{
|
||||
icon: <Icons.file className="h-5 w-5" />,
|
||||
iconMobile: <Icons.file className="h-5 w-5" />,
|
||||
label: "Help",
|
||||
href: "https://next-fast-turbo.mintlify.app/",
|
||||
pageTitle: "Documentation",
|
||||
navLocation: "bottom",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { content, theme } from "@/tailwind.config";
|
||||
import resolveConfig from "tailwindcss/resolveConfig";
|
||||
import type { DefaultColors } from "tailwindcss/types/generated/colors";
|
||||
|
||||
export const fullTwConfig = resolveConfig({
|
||||
content,
|
||||
theme,
|
||||
});
|
||||
|
||||
interface TailwindCustomColours extends DefaultColors {
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
background: string;
|
||||
foreground: string;
|
||||
primary: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
secondary: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
destructive: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
muted: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
accent: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
popover: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
card: {
|
||||
DEFAULT: string;
|
||||
foreground: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const twColourConfig: TailwindCustomColours = fullTwConfig.theme
|
||||
.colors as TailwindCustomColours;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
4
apps/web/next.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
@ -1,61 +1,27 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev:turbo": "next dev --turbo",
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"build:turbo": "turbo build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"ui:add": "pnpm dlx shadcn-ui@latest add",
|
||||
"generate-client": "openapi --input https://next-fast-turbo-api.vercel.app/openapi.json --output ./lib/api/client --client axios --useOptions --useUnionTypes",
|
||||
"generate-client:dev": "openapi --input http://127.0.0.0:8000/openapi.json --output ./lib/api/client --client axios --useOptions --useUnionTypes"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"axios": "^1.6.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"framer-motion": "^11.0.5",
|
||||
"hamburger-react": "^2.5.0",
|
||||
"lucide-react": "^0.220.0",
|
||||
"next": "^14.1.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-use": "^17.5.0",
|
||||
"recharts": "^2.12.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-text-fill": "^0.2.0",
|
||||
"zod": "^3.22.4"
|
||||
"@repo/ui": "workspace:*",
|
||||
"react": "19.0.0-rc-f994737d14-20240522",
|
||||
"react-dom": "19.0.0-rc-f994737d14-20240522",
|
||||
"next": "15.0.0-rc.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^14.0.4",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/eslint": "^8.56.1",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-turbo": "^1.11.3",
|
||||
"openapi-typescript-codegen": "^0.27.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.0-rc.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 565 B |
|
Before Width: | Height: | Size: 755 B |
3
apps/web/public/file-text.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 13.5V6.5V5.41421C14.5 5.149 14.3946 4.89464 14.2071 4.70711L9.79289 0.292893C9.60536 0.105357 9.351 0 9.08579 0H8H3H1.5V1.5V13.5C1.5 14.8807 2.61929 16 4 16H12C13.3807 16 14.5 14.8807 14.5 13.5ZM13 13.5V6.5H9.5H8V5V1.5H3V13.5C3 14.0523 3.44772 14.5 4 14.5H12C12.5523 14.5 13 14.0523 13 13.5ZM9.5 5V2.12132L12.3787 5H9.5ZM5.13 5.00062H4.505V6.25062H5.13H6H6.625V5.00062H6H5.13ZM4.505 8H5.13H11H11.625V9.25H11H5.13H4.505V8ZM5.13 11H4.505V12.25H5.13H11H11.625V11H11H5.13Z" fill="#666666"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 645 B |
10
apps/web/public/globe.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_868_525)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z" fill="#666666"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_868_525">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,20 +0,0 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #020000;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 8px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-1" cx="50" cy="50" r="49.9" />
|
||||
<path class="cls-2" d="M61.77,69.61,81.38,50,61.77,30.39" />
|
||||
<path class="cls-2" d="M38.23,69.61,18.62,50,38.23,30.39" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 616 B |
1
apps/web/public/next.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
apps/web/public/vercel.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_977_547)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3L18.5 17H2.5L10.5 3Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_977_547">
|
||||
<rect width="16" height="16" fill="white" transform="translate(2.5 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
3
apps/web/public/window.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM3.75 5.5C4.16421 5.5 4.5 5.16421 4.5 4.75C4.5 4.33579 4.16421 4 3.75 4C3.33579 4 3 4.33579 3 4.75C3 5.16421 3.33579 5.5 3.75 5.5ZM7 4.75C7 5.16421 6.66421 5.5 6.25 5.5C5.83579 5.5 5.5 5.16421 5.5 4.75C5.5 4.33579 5.83579 4 6.25 4C6.66421 4 7 4.33579 7 4.75ZM8.75 5.5C9.16421 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.16421 4 8.75 4C8.33579 4 8 4.33579 8 4.75C8 5.16421 8.33579 5.5 8.75 5.5Z" fill="#666666"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 750 B |
|
|
@ -1,84 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
const { fontFamily } = require("tailwindcss/defaultTheme");
|
||||
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
// tailwindcss-text-fill - used for overriding styling of autofill inputs in forms: https://www.npmjs.com/package/tailwindcss-text-fill
|
||||
// tailwindcss-animate - adds animation utilities: https://github.com/jamiebuilds/tailwindcss-animate
|
||||
plugins: [require("tailwindcss-animate"), require("tailwindcss-text-fill")],
|
||||
};
|
||||
|
|
@ -1,22 +1,15 @@
|
|||
{
|
||||
"extends": "@repo/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/lib/*": ["./lib/*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"next.config.js",
|
||||
"next.config.mjs",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
|
|
|
|||