feat(create-turbo): apply official-starter transform

This commit is contained in:
Turbobot 2024-08-11 23:39:29 -07:00 committed by DESKTOP-RTLN3BA\$punk
parent 55332d1ddb
commit 856eb69577
201 changed files with 2812 additions and 14413 deletions

36
apps/web/.gitignore vendored Normal file
View 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

View file

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

View 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;
}
}
}

View file

@ -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>
);

View 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();
}
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -1,3 +0,0 @@
export default function Page() {
return <div>Settings page</div>;
}

View file

@ -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"
}
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
),
};

View file

@ -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)
)
);
}

View file

@ -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>
);
}

View file

@ -1 +0,0 @@
export { default as DashboardLayout } from "../dashboard/DashboardLayout";

View file

@ -1,4 +0,0 @@
/* Vars here are coming from /DashboardLayout.module.css */
.dashboardFooter {
@apply mt-[var(--small-spacing)] min-h-[var(--footer-height)];
}

View file

@ -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;

View file

@ -1 +0,0 @@
export { default } from "./Footer";

View file

@ -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;

View file

@ -1 +0,0 @@
export { default } from "./Header";

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,2 +0,0 @@
export { default as Sidebar } from "./Sidebar";
export { default as SidebarMobile } from "./SidebarMobile";

View file

@ -1,4 +0,0 @@
export { Sidebar } from "./Sidebar";
export { SidebarMobile } from "./Sidebar";
export { default as Header } from "./Header";
export { default as Footer } from "./Footer";

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
</>
);
}

View file

@ -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>;
}

View file

@ -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%;
}
}

View file

@ -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 };

View file

@ -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 };

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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;
}
}

View file

@ -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>;
};

View file

@ -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;
};

View file

@ -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;
}
}

View file

@ -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,
};

View file

@ -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);
}
});
};

View file

@ -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";

View file

@ -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>;
};

View file

@ -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;
};

View file

@ -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>;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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>;
};

View file

@ -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;
};

View file

@ -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`,
},
});
}
}

View file

@ -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`,
},
});
}
}

View file

@ -1 +0,0 @@
export * from "./nav.tsx";

View file

@ -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",
},
],
};

View file

@ -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;

View file

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

View file

@ -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
View file

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

View file

@ -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"
}
}
}

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 B

View 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
View 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

View file

@ -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
View 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

View 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

View 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

View file

@ -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")],
};

View file

@ -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"