diff --git a/apps/public/.gitignore b/apps/public/.gitignore
new file mode 100644
index 00000000..2971a0bd
--- /dev/null
+++ b/apps/public/.gitignore
@@ -0,0 +1,42 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# database
+/prisma/db.sqlite
+/prisma/db.sqlite-journal
+
+# next.js
+/.next/
+/out/
+next-env.d.ts
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
+.env
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
diff --git a/apps/public/Dockerfile b/apps/public/Dockerfile
new file mode 100644
index 00000000..4ce0fd31
--- /dev/null
+++ b/apps/public/Dockerfile
@@ -0,0 +1,76 @@
+# Dockerfile that builds the web app only
+
+FROM --platform=linux/amd64 node:20-slim AS base
+
+ARG DATABASE_URL
+ENV DATABASE_URL=$DATABASE_URL
+
+ENV PNPM_HOME="/pnpm"
+
+ENV PATH="$PNPM_HOME:$PATH"
+
+RUN corepack enable
+
+ARG NODE_VERSION=20
+
+RUN apt update \
+ && apt install -y curl \
+ && curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
+ && bash n $NODE_VERSION \
+ && rm n \
+ && npm install -g n
+
+WORKDIR /app
+
+COPY package.json package.json
+COPY pnpm-lock.yaml pnpm-lock.yaml
+COPY pnpm-workspace.yaml pnpm-workspace.yaml
+COPY apps/public/package.json apps/public/package.json
+COPY packages/db/package.json packages/db/package.json
+COPY packages/queue/package.json packages/queue/package.json
+COPY packages/types/package.json packages/types/package.json
+
+# BUILD
+FROM base AS build
+
+WORKDIR /app/apps/public
+RUN pnpm install --frozen-lockfile --ignore-scripts
+
+WORKDIR /app
+COPY apps apps
+COPY packages packages
+COPY tooling tooling
+RUN pnpm db:codegen
+
+WORKDIR /app/apps/public
+RUN pnpm run build
+
+# PROD
+FROM base AS prod
+
+WORKDIR /app/apps/public
+RUN pnpm install --frozen-lockfile --prod --ignore-scripts
+
+# FINAL
+FROM base AS runner
+
+COPY --from=build /app/package.json /app/package.json
+COPY --from=prod /app/node_modules /app/node_modules
+# Apps
+COPY --from=build /app/apps/public /app/apps/public
+# Apps node_modules
+COPY --from=prod /app/apps/public/node_modules /app/apps/public/node_modules
+# Packages
+COPY --from=build /app/packages/db /app/packages/db
+# COPY --from=build /app/packages/queue /app/packages/queue
+# Packages node_modules
+COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
+# COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
+
+RUN pnpm db:codegen
+
+WORKDIR /app/apps/public
+
+EXPOSE 3000
+
+CMD ["pnpm", "start"]
\ No newline at end of file
diff --git a/apps/public/README.md b/apps/public/README.md
new file mode 100644
index 00000000..fba19eda
--- /dev/null
+++ b/apps/public/README.md
@@ -0,0 +1,28 @@
+# Create T3 App
+
+This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
+
+## What's next? How do I make an app with this?
+
+We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
+
+If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
+
+- [Next.js](https://nextjs.org)
+- [NextAuth.js](https://next-auth.js.org)
+- [Prisma](https://prisma.io)
+- [Tailwind CSS](https://tailwindcss.com)
+- [tRPC](https://trpc.io)
+
+## Learn More
+
+To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
+
+- [Documentation](https://create.t3.gg/)
+- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) β Check out these awesome tutorials
+
+You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) β your feedback and contributions are welcome!
+
+## How do I deploy this?
+
+Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
diff --git a/apps/public/components.json b/apps/public/components.json
new file mode 100644
index 00000000..9750ef2a
--- /dev/null
+++ b/apps/public/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/styles/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/utils/cn"
+ }
+}
diff --git a/apps/public/next.config.mjs b/apps/public/next.config.mjs
new file mode 100644
index 00000000..e5bcbe13
--- /dev/null
+++ b/apps/public/next.config.mjs
@@ -0,0 +1,28 @@
+/**
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
+ * for Docker builds.
+ */
+await import('./src/env.mjs');
+
+/** @type {import("next").NextConfig} */
+const config = {
+ reactStrictMode: true,
+ transpilePackages: ['@mixan/queue'],
+ eslint: { ignoreDuringBuilds: true },
+ typescript: { ignoreBuildErrors: true },
+ experimental: {
+ // Avoid "Critical dependency: the request of a dependency is an expression"
+ serverComponentsExternalPackages: ['bullmq'],
+ },
+ /**
+ * If you are using `appDir` then you must comment the below `i18n` config out.
+ *
+ * @see https://github.com/vercel/next.js/issues/41980
+ */
+ i18n: {
+ locales: ['en'],
+ defaultLocale: 'en',
+ },
+};
+
+export default config;
diff --git a/apps/public/package.json b/apps/public/package.json
new file mode 100644
index 00000000..4f7121e5
--- /dev/null
+++ b/apps/public/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "@mixan/public",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "rm -rf .next && pnpm with-env next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint .",
+ "format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
+ "typecheck": "tsc --noEmit",
+ "with-env": "dotenv -e ../../.env -c --"
+ },
+ "dependencies": {
+ "@mixan/db": "workspace:*",
+ "@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-aspect-ratio": "^1.0.3",
+ "@radix-ui/react-avatar": "^1.0.4",
+ "@radix-ui/react-checkbox": "^1.0.4",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-scroll-area": "^1.0.5",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-toast": "^1.1.5",
+ "@radix-ui/react-tooltip": "^1.0.7",
+ "@t3-oss/env-nextjs": "^0.7.0",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.0.0",
+ "email-validator": "^2.0.4",
+ "embla-carousel-autoplay": "8.0.0-rc22",
+ "embla-carousel-react": "8.0.0-rc22",
+ "hamburger-react": "^2.5.0",
+ "lucide-react": "^0.286.0",
+ "next": "~14.0.4",
+ "nuqs": "^1.15.2",
+ "react": "18.2.0",
+ "react-animate-height": "^3.2.3",
+ "react-dom": "18.2.0",
+ "react-in-viewport": "1.0.0-alpha.30",
+ "react-responsive": "^9.0.2",
+ "react-syntax-highlighter": "^15.5.0",
+ "tailwind-merge": "^1.14.0",
+ "tailwindcss-animate": "^1.0.7",
+ "usehooks-ts": "^2.9.1",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@mixan/eslint-config": "workspace:*",
+ "@mixan/prettier-config": "workspace:*",
+ "@mixan/tsconfig": "workspace:*",
+ "@types/bcrypt": "^5.0.0",
+ "@types/lodash.debounce": "^4.0.9",
+ "@types/node": "^18.16.0",
+ "@types/ramda": "^0.29.6",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-syntax-highlighter": "^15.5.9",
+ "@typescript-eslint/eslint-plugin": "^6.6.0",
+ "@typescript-eslint/parser": "^6.6.0",
+ "autoprefixer": "^10.4.14",
+ "eslint": "^8.48.0",
+ "postcss": "^8.4.27",
+ "prettier": "^3.0.3",
+ "prettier-plugin-tailwindcss": "^0.5.1",
+ "tailwindcss": "^3.3.3",
+ "typescript": "^5.2.2"
+ },
+ "ct3aMetadata": {
+ "initVersion": "7.21.0"
+ },
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@mixan/eslint-config/base",
+ "@mixan/eslint-config/react",
+ "@mixan/eslint-config/nextjs"
+ ]
+ },
+ "prettier": "@mixan/prettier-config"
+}
diff --git a/apps/public/postcss.config.cjs b/apps/public/postcss.config.cjs
new file mode 100644
index 00000000..e305dd92
--- /dev/null
+++ b/apps/public/postcss.config.cjs
@@ -0,0 +1,8 @@
+const config = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+
+module.exports = config;
diff --git a/apps/public/public/demo/bar-min.png b/apps/public/public/demo/bar-min.png
new file mode 100644
index 00000000..9d723b55
Binary files /dev/null and b/apps/public/public/demo/bar-min.png differ
diff --git a/apps/public/public/demo/events-min.png b/apps/public/public/demo/events-min.png
new file mode 100644
index 00000000..cb36a7f7
Binary files /dev/null and b/apps/public/public/demo/events-min.png differ
diff --git a/apps/public/public/demo/histogram-min.png b/apps/public/public/demo/histogram-min.png
new file mode 100644
index 00000000..745612c5
Binary files /dev/null and b/apps/public/public/demo/histogram-min.png differ
diff --git a/apps/public/public/demo/line-min.png b/apps/public/public/demo/line-min.png
new file mode 100644
index 00000000..0256db75
Binary files /dev/null and b/apps/public/public/demo/line-min.png differ
diff --git a/apps/public/public/demo/metrics-min.png b/apps/public/public/demo/metrics-min.png
new file mode 100644
index 00000000..fbfe7455
Binary files /dev/null and b/apps/public/public/demo/metrics-min.png differ
diff --git a/apps/public/public/demo/overview-min.png b/apps/public/public/demo/overview-min.png
new file mode 100644
index 00000000..5f6d2612
Binary files /dev/null and b/apps/public/public/demo/overview-min.png differ
diff --git a/apps/public/public/demo/overview-share-min.png b/apps/public/public/demo/overview-share-min.png
new file mode 100644
index 00000000..3228c6b1
Binary files /dev/null and b/apps/public/public/demo/overview-share-min.png differ
diff --git a/apps/public/public/demo/pie-min.png b/apps/public/public/demo/pie-min.png
new file mode 100644
index 00000000..d28d0a61
Binary files /dev/null and b/apps/public/public/demo/pie-min.png differ
diff --git a/apps/public/public/demo/worldmap-min.png b/apps/public/public/demo/worldmap-min.png
new file mode 100644
index 00000000..90b412f4
Binary files /dev/null and b/apps/public/public/demo/worldmap-min.png differ
diff --git a/apps/public/public/logo.svg b/apps/public/public/logo.svg
new file mode 100644
index 00000000..e45d0d95
--- /dev/null
+++ b/apps/public/public/logo.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/apps/public/src/app/api/waitlist/route.ts b/apps/public/src/app/api/waitlist/route.ts
new file mode 100644
index 00000000..b221590e
--- /dev/null
+++ b/apps/public/src/app/api/waitlist/route.ts
@@ -0,0 +1,28 @@
+import * as EmailValidator from 'email-validator';
+// true
+
+import { NextResponse } from 'next/server';
+
+import { db } from '@mixan/db';
+
+EmailValidator.validate('test@email.com');
+
+export async function POST(req: Request) {
+ const body = await req.json();
+
+ if (!body.email) {
+ return NextResponse.json({ error: 'Email is required' }, { status: 400 });
+ }
+
+ if (!EmailValidator.validate(body.email)) {
+ return NextResponse.json({ error: 'Email is not valid' }, { status: 400 });
+ }
+
+ await db.waitlist.create({
+ data: {
+ email: body.email.toLowerCase(),
+ },
+ });
+
+ return NextResponse.json(body);
+}
diff --git a/apps/public/src/app/carousel.tsx b/apps/public/src/app/carousel.tsx
new file mode 100644
index 00000000..f26569d0
--- /dev/null
+++ b/apps/public/src/app/carousel.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious,
+} from '@/components/ui/carousel';
+import Autoplay from 'embla-carousel-autoplay';
+import Image from 'next/image';
+
+const images = [
+ {
+ title: 'Beautiful overview, everything is clickable to get more details',
+ url: '/demo/overview-min.png',
+ },
+ {
+ title: 'Histogram, perfect for showing active users',
+ url: '/demo/histogram-min.png',
+ },
+ { title: 'Make your overview public', url: '/demo/overview-share-min.png' },
+ {
+ title: 'See real time events from your users',
+ url: '/demo/events-min.png',
+ },
+ { title: 'The classic line chart', url: '/demo/line-min.png' },
+ {
+ title: 'Bar charts to see your most popular content',
+ url: '/demo/bar-min.png',
+ },
+ { title: 'Get nice metric cards with graphs', url: '/demo/metrics-min.png' },
+ { title: 'See where your events comes from', url: '/demo/worldmap-min.png' },
+ { title: 'The classic pie chart', url: '/demo/pie-min.png' },
+];
+
+export function HomeCarousel() {
+ return (
+
+
+
+
+
+ {images.map((item) => (
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/public/src/app/copy.tsx b/apps/public/src/app/copy.tsx
new file mode 100644
index 00000000..5f2b808b
--- /dev/null
+++ b/apps/public/src/app/copy.tsx
@@ -0,0 +1,34 @@
+import { cn } from '@/utils/cn';
+
+interface Props {
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function Lead({ children, className }: Props) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function Paragraph({ children, className }: Props) {
+ return {children}
;
+}
+
+export function Heading1({ children, className }: Props) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function Heading2({ children, className }: Props) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/public/src/app/favicon.ico b/apps/public/src/app/favicon.ico
new file mode 100644
index 00000000..dd5ab726
Binary files /dev/null and b/apps/public/src/app/favicon.ico differ
diff --git a/apps/public/src/app/join-waitlist.tsx b/apps/public/src/app/join-waitlist.tsx
new file mode 100644
index 00000000..5592104a
--- /dev/null
+++ b/apps/public/src/app/join-waitlist.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+
+export function JoinWaitlist() {
+ const [value, setValue] = useState('');
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+
+
+
+ Thanks so much!
+
+ You're now on the waiting list. We'll let you know when we're
+ ready. Should be within a month or two π
+
+
+
+
+ setOpen(false)}>Got it!
+
+
+
+
+ >
+ );
+}
diff --git a/apps/public/src/app/layout.tsx b/apps/public/src/app/layout.tsx
new file mode 100644
index 00000000..a6f0af4a
--- /dev/null
+++ b/apps/public/src/app/layout.tsx
@@ -0,0 +1,28 @@
+import { cn } from '@/utils/cn';
+
+import '@/styles/globals.css';
+
+import type { Metadata } from 'next';
+
+import { defaultMeta } from './meta';
+
+export const metadata: Metadata = {
+ ...defaultMeta,
+ alternates: {
+ canonical: 'https://openpanel.dev',
+ },
+};
+
+export default async function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/public/src/app/manifest.ts b/apps/public/src/app/manifest.ts
new file mode 100644
index 00000000..778ac20b
--- /dev/null
+++ b/apps/public/src/app/manifest.ts
@@ -0,0 +1,22 @@
+import type { MetadataRoute } from 'next';
+
+import { defaultMeta } from './meta';
+
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: defaultMeta.title as string,
+ short_name: 'Openpanel.dev',
+ description: defaultMeta.description!,
+ start_url: '/',
+ display: 'standalone',
+ background_color: '#fff',
+ theme_color: '#fff',
+ icons: [
+ {
+ src: '/favicon.ico',
+ sizes: 'any',
+ type: 'image/x-icon',
+ },
+ ],
+ };
+}
diff --git a/apps/public/src/app/meta.ts b/apps/public/src/app/meta.ts
new file mode 100644
index 00000000..ee4951fe
--- /dev/null
+++ b/apps/public/src/app/meta.ts
@@ -0,0 +1,7 @@
+import type { Metadata } from 'next';
+
+export const defaultMeta: Metadata = {
+ title: 'Openpanel.dev | A Open-Source Analytics Library',
+ description:
+ 'Unlock actionable insights effortlessly with Insightful, the open-source analytics library that combines the power of Mixpanel with the simplicity of Plausible. Enjoy a unified overview, predictable pricing, and a vibrant community. Join us in democratizing analytics today!',
+};
diff --git a/apps/public/src/app/page.tsx b/apps/public/src/app/page.tsx
new file mode 100644
index 00000000..6d4ec140
--- /dev/null
+++ b/apps/public/src/app/page.tsx
@@ -0,0 +1,158 @@
+import { Logo } from '@/components/Logo';
+import { Button } from '@/components/ui/button';
+import {
+ BarChart2,
+ CookieIcon,
+ Globe2Icon,
+ LayoutPanelTopIcon,
+ LockIcon,
+ UsersIcon,
+} from 'lucide-react';
+
+import { HomeCarousel } from './carousel';
+import { Heading1, Heading2, Lead, Paragraph } from './copy';
+import { JoinWaitlist } from './join-waitlist';
+
+const features = [
+ {
+ title: 'Great overview',
+ icon: LayoutPanelTopIcon,
+ },
+ {
+ title: 'Beautiful charts',
+ icon: BarChart2,
+ },
+ {
+ title: 'Privacy focused',
+ icon: LockIcon,
+ },
+ {
+ title: 'Open-source',
+ icon: Globe2Icon,
+ },
+ {
+ title: 'No cookies',
+ icon: CookieIcon,
+ },
+ {
+ title: 'User journey',
+ icon: UsersIcon,
+ },
+];
+
+export default function Page() {
+ return (
+
+
+
+
+
+
+ A open-source
+
+ alternative to Mixpanel
+
+
+ Combine Mixpanel and Plausible and you get Openpanel. A simple
+ analytics tool that respects privacy.
+
+
+
+ {features.map(({ icon: Icon, title }) => (
+
+
+ {title}
+
+ ))}
+
+
+
+
+
+ Get a feel how it looks
+
+
+ We've crafted a clean and intuitive interface because analytics should
+
+ be straightforward, unlike the complexity often associated with Google
+ Analytics. π
+
+
+
+
+
Another analytic tool?
+
+
+ TL;DR Our open-source analytic library fills a
+ crucial gap by combining the strengths of Mixpanel's powerful
+ features with Plausible's clear overview page. Motivated by the lack
+ of an open-source alternative to Mixpanel and inspired by
+ Plausible's simplicity, we aim to create an intuitive platform with
+ predictable pricing. With a single-tier pricing model and limits
+ only on monthly event counts, our goal is to democratize analytics,
+ offering unrestricted access to all features while ensuring
+ affordability and transparency for users of all project sizes.
+
+
+
+
+
+ Our open-source analytic library emerged from a clear need within
+ the analytics community. While platforms like Mixpanel offer
+ powerful and user-friendly features, they lack a comprehensive
+ overview page akin to Plausible's, which succinctly summarizes
+ essential metrics. Recognizing this gap, we saw an opportunity to
+ combine the strengths of both platforms while addressing their
+ respective shortcomings.
+
+
+
+ One significant motivation behind our endeavor was the absence of an
+ open-source alternative to Mixpanel. We believe in the importance of
+ accessibility and transparency in analytics, which led us to embark
+ on creating a solution that anyone can freely use and contribute to.
+
+
+
+ Inspired by Plausible's exemplary approach to simplicity and
+ clarity, we aim to build upon their foundation and further refine
+ the user experience. By harnessing the best practices demonstrated
+ by Plausible, we aspire to create an intuitive and streamlined
+ analytics platform that empowers users to derive actionable insights
+ effortlessly.
+
+
+
+ Our own experiences with traditional analytics platforms like
+ Mixpanel underscored another critical aspect driving our project:
+ the need for predictable pricing. As project owners ourselves, we
+ encountered the frustration of escalating costs as our user base
+ grew. Therefore, we are committed to offering a single-tier pricing
+ model that provides unlimited access to all features without the
+ fear of unexpected expenses.
+
+
+
+ In line with our commitment to fairness and accessibility, our
+ pricing model will only impose limits on the number of events users
+ can send each month. This approach, akin to Plausible's, ensures
+ that users have the freedom to explore and utilize our platform to
+ its fullest potential without arbitrary restrictions on reports or
+ user counts. Ultimately, our goal is to democratize analytics by
+ offering a reliable, transparent, and cost-effective solution for
+ projects of all sizes.
+
+
+
+
+ );
+}
diff --git a/apps/public/src/components/Logo.tsx b/apps/public/src/components/Logo.tsx
new file mode 100644
index 00000000..579ef078
--- /dev/null
+++ b/apps/public/src/components/Logo.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@/utils/cn';
+import Image from 'next/image';
+
+interface LogoProps {
+ className?: string;
+}
+
+export function Logo({ className }: LogoProps) {
+ return (
+
+
+ openpanel.dev
+
+ );
+}
diff --git a/apps/public/src/components/ui/RenderDots.tsx b/apps/public/src/components/ui/RenderDots.tsx
new file mode 100644
index 00000000..267c42e7
--- /dev/null
+++ b/apps/public/src/components/ui/RenderDots.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { cn } from '@/utils/cn';
+import { Asterisk, ChevronRight } from 'lucide-react';
+
+import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
+
+interface RenderDotsProps extends React.HTMLAttributes {
+ children: string;
+ truncate?: boolean;
+}
+
+export function RenderDots({
+ children,
+ className,
+ truncate,
+ ...props
+}: RenderDotsProps) {
+ const parts = children.split('.');
+ const sliceAt = truncate && parts.length > 3 ? 3 : 0;
+ return (
+
+
+
+ {parts.slice(-sliceAt).map((str, index) => {
+ return (
+
+ {index !== 0 && (
+
+ )}
+ {str.includes('[*]') ? (
+ <>
+ {str.replace('[*]', '')}
+
+ >
+ ) : str === '*' ? (
+
+ ) : (
+ str
+ )}
+
+ );
+ })}
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/public/src/components/ui/alert-dialog.tsx b/apps/public/src/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..463dc1e2
--- /dev/null
+++ b/apps/public/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,140 @@
+'use client';
+
+import * as React from 'react';
+import { buttonVariants } from '@/components/ui/button';
+import { cn } from '@/utils/cn';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = 'AlertDialogHeader';
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = 'AlertDialogFooter';
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/apps/public/src/components/ui/alert.tsx b/apps/public/src/components/ui/alert.tsx
new file mode 100644
index 00000000..27093d2a
--- /dev/null
+++ b/apps/public/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive text-destructive dark:border-destructive [&>svg]:text-destructive',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/apps/public/src/components/ui/aspect-ratio.tsx b/apps/public/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 00000000..aaabffbc
--- /dev/null
+++ b/apps/public/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
+
+const AspectRatio = AspectRatioPrimitive.Root;
+
+export { AspectRatio };
diff --git a/apps/public/src/components/ui/avatar.tsx b/apps/public/src/components/ui/avatar.tsx
new file mode 100644
index 00000000..2817f1c6
--- /dev/null
+++ b/apps/public/src/components/ui/avatar.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/apps/public/src/components/ui/badge.tsx b/apps/public/src/components/ui/badge.tsx
new file mode 100644
index 00000000..60b45ea9
--- /dev/null
+++ b/apps/public/src/components/ui/badge.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-1.5 h-[20px] text-[10px] font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ success:
+ 'border-transparent bg-emerald-500 text-emerald-100 hover:bg-emerald-500/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/public/src/components/ui/button.tsx b/apps/public/src/components/ui/button.tsx
new file mode 100644
index 00000000..0ab333b1
--- /dev/null
+++ b/apps/public/src/components/ui/button.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import { Slot } from '@radix-ui/react-slot';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+import type { LucideIcon } from 'lucide-react';
+import { Loader2 } from 'lucide-react';
+
+const buttonVariants = cva(
+ 'flex-shrink-0 inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background whitespace-nowrap 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',
+ cta: 'bg-blue-600 text-primary-foreground hover:bg-blue-500',
+ 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,
+ VariantProps {
+ asChild?: boolean;
+ loading?: boolean;
+ icon?: LucideIcon;
+}
+
+const Button = React.forwardRef(
+ (
+ {
+ className,
+ variant,
+ size,
+ asChild = false,
+ children,
+ loading,
+ disabled,
+ icon,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : 'button';
+ const Icon = loading ? Loader2 : icon ?? null;
+ return (
+
+ {Icon && (
+
+ )}
+ {children}
+
+ );
+ }
+);
+Button.displayName = 'Button';
+Button.defaultProps = {
+ type: 'button',
+};
+
+export { Button, buttonVariants };
diff --git a/apps/public/src/components/ui/carousel.tsx b/apps/public/src/components/ui/carousel.tsx
new file mode 100644
index 00000000..80f28c1c
--- /dev/null
+++ b/apps/public/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/utils/cn"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/apps/public/src/components/ui/checkbox.tsx b/apps/public/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..f5253de3
--- /dev/null
+++ b/apps/public/src/components/ui/checkbox.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { Check } from 'lucide-react';
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/apps/public/src/components/ui/combobox-advanced.tsx b/apps/public/src/components/ui/combobox-advanced.tsx
new file mode 100644
index 00000000..8a608580
--- /dev/null
+++ b/apps/public/src/components/ui/combobox-advanced.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import * as React from 'react';
+import { Badge } from '@/components/ui/badge';
+import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
+import { useOnClickOutside } from 'usehooks-ts';
+
+import { Checkbox } from './checkbox';
+import { Input } from './input';
+
+type IValue = any;
+type IItem = Record<'value' | 'label', IValue>;
+
+interface ComboboxAdvancedProps {
+ value: IValue[];
+ onChange: React.Dispatch>;
+ items: IItem[];
+ placeholder: string;
+}
+
+export function ComboboxAdvanced({
+ items,
+ value,
+ onChange,
+ placeholder,
+}: ComboboxAdvancedProps) {
+ const [open, setOpen] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState('');
+ const ref = React.useRef(null);
+ useOnClickOutside(ref, () => setOpen(false));
+
+ const selectables = items
+ .filter((item) => !value.find((s) => s === item.value))
+ .filter(
+ (item) =>
+ (typeof item.label === 'string' &&
+ item.label.toLowerCase().includes(inputValue.toLowerCase())) ||
+ (typeof item.value === 'string' &&
+ item.value.toLowerCase().includes(inputValue.toLowerCase()))
+ );
+
+ const renderItem = (item: IItem) => {
+ const checked = !!value.find((s) => s === item.value);
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onSelect={() => {
+ setInputValue('');
+ onChange((prev) => {
+ if (prev.includes(item.value)) {
+ return prev.filter((s) => s !== item.value);
+ }
+ return [...prev, item.value];
+ });
+ }}
+ className={'cursor-pointer flex items-center gap-2'}
+ >
+
+ {item?.label ?? item?.value}
+
+ );
+ };
+
+ const renderUnknownItem = (value: IValue) => {
+ const item = items.find((item) => item.value === value);
+ return item ? renderItem(item) : renderItem({ value, label: value });
+ };
+
+ return (
+
+ setOpen((prev) => !prev)}
+ >
+
+ {value.length === 0 && placeholder}
+ {value.slice(0, 2).map((value) => {
+ const item = items.find((item) => item.value === value) ?? {
+ value,
+ label: value,
+ };
+ return (
+
+ {item.label}
+
+ );
+ })}
+ {value.length > 2 && (
+ +{value.length - 2} more
+ )}
+
+
+ {open && (
+
+
+
+
+ setInputValue(event.target.value)}
+ />
+
+ {inputValue === ''
+ ? value.map(renderUnknownItem)
+ : renderItem({
+ value: inputValue,
+ label: `Pick "${inputValue}"`,
+ })}
+ {selectables.map(renderItem)}
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/public/src/components/ui/combobox-multi.tsx b/apps/public/src/components/ui/combobox-multi.tsx
new file mode 100644
index 00000000..34a132a9
--- /dev/null
+++ b/apps/public/src/components/ui/combobox-multi.tsx
@@ -0,0 +1,125 @@
+'use client';
+
+import * as React from 'react';
+import { Badge } from '@/components/ui/badge';
+import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
+import { Command as CommandPrimitive } from 'cmdk';
+import { X } from 'lucide-react';
+
+type Item = Record<'value' | 'label', string>;
+
+interface ComboboxMultiProps {
+ selected: Item[];
+ setSelected: React.Dispatch>;
+ items: Item[];
+ placeholder: string;
+}
+
+export function ComboboxMulti({
+ items,
+ selected,
+ setSelected,
+ placeholder,
+ ...props
+}: ComboboxMultiProps) {
+ const inputRef = React.useRef(null);
+ const [open, setOpen] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState('');
+
+ const handleUnselect = React.useCallback((item: Item) => {
+ setSelected((prev) => prev.filter((s) => s.value !== item.value));
+ }, []);
+
+ const handleKeyDown = React.useCallback(
+ (e: React.KeyboardEvent) => {
+ const input = inputRef.current;
+ if (input) {
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ if (input.value === '') {
+ setSelected((prev) => {
+ const newSelected = [...prev];
+ newSelected.pop();
+ return newSelected;
+ });
+ }
+ }
+ // This is not a default behaviour of the field
+ if (e.key === 'Escape') {
+ input.blur();
+ }
+ }
+ },
+ []
+ );
+
+ const selectables = items.filter(
+ (item) => !selected.find((s) => s.value === item.value)
+ );
+
+ return (
+
+
+
+ {selected.map((item) => {
+ return (
+
+ {item.label}
+ {
+ if (e.key === 'Enter') {
+ handleUnselect(item);
+ }
+ }}
+ onMouseDown={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onClick={() => handleUnselect(item)}
+ >
+
+
+
+ );
+ })}
+ {/* Avoid having the "Search" Icon */}
+ setOpen(false)}
+ onFocus={() => setOpen(true)}
+ placeholder={placeholder}
+ className="ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1"
+ />
+
+
+
+ {open && selectables.length > 0 ? (
+
+
+ {selectables.map((item) => {
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onSelect={(value) => {
+ setInputValue('');
+ setSelected((prev) => [...prev, item]);
+ }}
+ className={'cursor-pointer'}
+ >
+ {item.label}
+
+ );
+ })}
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/public/src/components/ui/combobox.tsx b/apps/public/src/components/ui/combobox.tsx
new file mode 100644
index 00000000..a939345a
--- /dev/null
+++ b/apps/public/src/components/ui/combobox.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import * as React from 'react';
+import type { ButtonProps } from '@/components/ui/button';
+import { Button } from '@/components/ui/button';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from '@/components/ui/command';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { cn } from '@/utils/cn';
+import type { LucideIcon } from 'lucide-react';
+import { Check, ChevronsUpDown } from 'lucide-react';
+
+export interface ComboboxProps {
+ placeholder: string;
+ items: {
+ value: T;
+ label: string;
+ disabled?: boolean;
+ }[];
+ value: T | null | undefined;
+ onChange: (value: T) => void;
+ children?: React.ReactNode;
+ onCreate?: (value: T) => void;
+ className?: string;
+ searchable?: boolean;
+ icon?: LucideIcon;
+ size?: ButtonProps['size'];
+ label?: string;
+}
+
+export type ExtendedComboboxProps = Omit<
+ ComboboxProps,
+ 'items' | 'placeholder'
+> & {
+ placeholder?: string;
+};
+
+export function Combobox({
+ placeholder,
+ items,
+ value,
+ onChange,
+ children,
+ onCreate,
+ className,
+ searchable,
+ icon: Icon,
+ size,
+ label,
+}: ComboboxProps) {
+ const [open, setOpen] = React.useState(false);
+ const [search, setSearch] = React.useState('');
+ function find(value: string) {
+ return items.find(
+ (item) => item.value.toLowerCase() === value.toLowerCase()
+ );
+ }
+
+ return (
+
+
+ {children ?? (
+
+ {Icon ? : null}
+
+ {value ? find(value)?.label ?? 'No match' : placeholder}
+
+
+
+ )}
+
+
+
+ {searchable === true && (
+
+ )}
+ {typeof onCreate === 'function' && search ? (
+
+ {
+ onCreate(search as T);
+ setSearch('');
+ setOpen(false);
+ }}
+ >
+ Create "{search}"
+
+
+ ) : (
+ Nothing selected
+ )}
+
+
+ {items.map((item) => (
+ {
+ const value = find(currentValue)?.value ?? currentValue;
+ onChange(value as T);
+ setOpen(false);
+ }}
+ {...(item.disabled && { disabled: true })}
+ >
+
+ {item.label}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/public/src/components/ui/command.tsx b/apps/public/src/components/ui/command.tsx
new file mode 100644
index 00000000..91b9f2af
--- /dev/null
+++ b/apps/public/src/components/ui/command.tsx
@@ -0,0 +1,155 @@
+'use client';
+
+import * as React from 'react';
+import { Dialog, DialogContent } from '@/components/ui/dialog';
+import { cn } from '@/utils/cn';
+import type { DialogProps } from '@radix-ui/react-dialog';
+import { Command as CommandPrimitive } from 'cmdk';
+import { Search } from 'lucide-react';
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Command.displayName = CommandPrimitive.displayName;
+
+type CommandDialogProps = DialogProps;
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandList.displayName = CommandPrimitive.List.displayName;
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+));
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+CommandShortcut.displayName = 'CommandShortcut';
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+};
diff --git a/apps/public/src/components/ui/dialog.tsx b/apps/public/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..fe85da54
--- /dev/null
+++ b/apps/public/src/components/ui/dialog.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/apps/public/src/components/ui/dropdown-menu.tsx b/apps/public/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..842e461c
--- /dev/null
+++ b/apps/public/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,199 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+
+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,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/apps/public/src/components/ui/input.tsx b/apps/public/src/components/ui/input.tsx
new file mode 100644
index 00000000..3a6d2553
--- /dev/null
+++ b/apps/public/src/components/ui/input.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+
+export type InputProps = React.InputHTMLAttributes & {
+ error?: string | undefined;
+};
+
+const Input = React.forwardRef(
+ ({ className, error, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/apps/public/src/components/ui/label.tsx b/apps/public/src/components/ui/label.tsx
new file mode 100644
index 00000000..19859916
--- /dev/null
+++ b/apps/public/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+
+const labelVariants = cva(
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-2'
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/apps/public/src/components/ui/popover.tsx b/apps/public/src/components/ui/popover.tsx
new file mode 100644
index 00000000..b86795cd
--- /dev/null
+++ b/apps/public/src/components/ui/popover.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as PopoverPrimitive from '@radix-ui/react-popover';
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
+ <>
+
+ >
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/apps/public/src/components/ui/radio-group.tsx b/apps/public/src/components/ui/radio-group.tsx
new file mode 100644
index 00000000..fb251a99
--- /dev/null
+++ b/apps/public/src/components/ui/radio-group.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+
+export type RadioGroupProps = React.InputHTMLAttributes;
+export type RadioGroupItemProps =
+ React.InputHTMLAttributes & {
+ active?: boolean;
+ };
+
+const RadioGroup = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+
+const RadioGroupItem = React.forwardRef(
+ ({ className, active, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+
+RadioGroup.displayName = 'RadioGroup';
+RadioGroupItem.displayName = 'RadioGroupItem';
+
+export { RadioGroup, RadioGroupItem };
diff --git a/apps/public/src/components/ui/scroll-area.tsx b/apps/public/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..b2c7836c
--- /dev/null
+++ b/apps/public/src/components/ui/scroll-area.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/apps/public/src/components/ui/sheet.tsx b/apps/public/src/components/ui/sheet.tsx
new file mode 100644
index 00000000..d2a3ff6e
--- /dev/null
+++ b/apps/public/src/components/ui/sheet.tsx
@@ -0,0 +1,143 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as SheetPrimitive from '@radix-ui/react-dialog';
+import { ScrollArea } from '@radix-ui/react-scroll-area';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+import { X } from 'lucide-react';
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = SheetPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
+
+const sheetVariants = cva(
+ 'fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-150 data-[state=open]:duration-150',
+ {
+ variants: {
+ side: {
+ top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
+ bottom:
+ 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
+ left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
+ right:
+ 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
+ },
+ },
+ defaultVariants: {
+ side: 'right',
+ },
+ }
+);
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = 'right', className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+ Close
+
+
+
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = 'SheetHeader';
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = 'SheetFooter';
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/apps/public/src/components/ui/table.tsx b/apps/public/src/components/ui/table.tsx
new file mode 100644
index 00000000..fda58dff
--- /dev/null
+++ b/apps/public/src/components/ui/table.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes & {
+ wrapper?: boolean;
+ overflow?: boolean;
+ }
+>(({ className, wrapper, overflow = true, ...props }, ref) => (
+
+));
+Table.displayName = 'Table';
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHeader.displayName = 'TableHeader';
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableBody.displayName = 'TableBody';
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableFooter.displayName = 'TableFooter';
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableRow.displayName = 'TableRow';
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHead.displayName = 'TableHead';
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCell.displayName = 'TableCell';
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = 'TableCaption';
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/apps/public/src/components/ui/toast.tsx b/apps/public/src/components/ui/toast.tsx
new file mode 100644
index 00000000..aa5ebf9f
--- /dev/null
+++ b/apps/public/src/components/ui/toast.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as ToastPrimitives from '@radix-ui/react-toast';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+import { X } from 'lucide-react';
+
+const ToastProvider = ToastPrimitives.Provider;
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
+
+const toastVariants = cva(
+ 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
+ {
+ variants: {
+ variant: {
+ default: 'border bg-background text-foreground',
+ destructive:
+ 'destructive group border-destructive bg-destructive text-destructive-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+});
+Toast.displayName = ToastPrimitives.Root.displayName;
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastAction.displayName = ToastPrimitives.Action.displayName;
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ToastClose.displayName = ToastPrimitives.Close.displayName;
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastTitle.displayName = ToastPrimitives.Title.displayName;
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastDescription.displayName = ToastPrimitives.Description.displayName;
+
+type ToastProps = React.ComponentPropsWithoutRef;
+
+type ToastActionElement = React.ReactElement;
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+};
diff --git a/apps/public/src/components/ui/toaster.tsx b/apps/public/src/components/ui/toaster.tsx
new file mode 100644
index 00000000..beb7f25c
--- /dev/null
+++ b/apps/public/src/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from '@/components/ui/toast';
+import { useToast } from '@/components/ui/use-toast';
+
+export function Toaster() {
+ const { toasts } = useToast();
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title} }
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/public/src/components/ui/tooltip.tsx b/apps/public/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000..1849163f
--- /dev/null
+++ b/apps/public/src/components/ui/tooltip.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import * as TooltipPrimitive from '@radix-ui/react-tooltip';
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/public/src/components/ui/use-toast.ts b/apps/public/src/components/ui/use-toast.ts
new file mode 100644
index 00000000..e6807d07
--- /dev/null
+++ b/apps/public/src/components/ui/use-toast.ts
@@ -0,0 +1,190 @@
+'use client';
+
+// Inspired by react-hot-toast library
+import * as React from 'react';
+import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 1000000;
+
+type ToasterToast = ToastProps & {
+ id: string;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ action?: ToastActionElement;
+};
+
+const actionTypes = {
+ ADD_TOAST: 'ADD_TOAST',
+ UPDATE_TOAST: 'UPDATE_TOAST',
+ DISMISS_TOAST: 'DISMISS_TOAST',
+ REMOVE_TOAST: 'REMOVE_TOAST',
+} as const;
+
+let count = 0;
+
+function genId() {
+ count = (count + 1) % Number.MAX_VALUE;
+ return count.toString();
+}
+
+type ActionType = typeof actionTypes;
+
+type Action =
+ | {
+ type: ActionType['ADD_TOAST'];
+ toast: ToasterToast;
+ }
+ | {
+ type: ActionType['UPDATE_TOAST'];
+ toast: Partial;
+ }
+ | {
+ type: ActionType['DISMISS_TOAST'];
+ toastId?: ToasterToast['id'];
+ }
+ | {
+ type: ActionType['REMOVE_TOAST'];
+ toastId?: ToasterToast['id'];
+ };
+
+interface State {
+ toasts: ToasterToast[];
+}
+
+const toastTimeouts = new Map>();
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId);
+ dispatch({
+ type: 'REMOVE_TOAST',
+ toastId: toastId,
+ });
+ }, TOAST_REMOVE_DELAY);
+
+ toastTimeouts.set(toastId, timeout);
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'ADD_TOAST':
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ };
+
+ case 'UPDATE_TOAST':
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ };
+
+ case 'DISMISS_TOAST': {
+ const { toastId } = action;
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId);
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id);
+ });
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ };
+ }
+ case 'REMOVE_TOAST':
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ };
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ };
+ }
+};
+
+const listeners: ((state: State) => void)[] = [];
+
+let memoryState: State = { toasts: [] };
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action);
+ listeners.forEach((listener) => {
+ listener(memoryState);
+ });
+}
+
+export type Toast = Omit;
+
+function toast({ ...props }: Toast) {
+ const id = genId();
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: 'UPDATE_TOAST',
+ toast: { ...props, id },
+ });
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
+
+ dispatch({
+ type: 'ADD_TOAST',
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss();
+ },
+ },
+ });
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ };
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState);
+
+ React.useEffect(() => {
+ listeners.push(setState);
+ return () => {
+ const index = listeners.indexOf(setState);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+ };
+ }, [state]);
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
+ };
+}
+
+export { useToast, toast };
diff --git a/apps/public/src/env.mjs b/apps/public/src/env.mjs
new file mode 100644
index 00000000..32ac1455
--- /dev/null
+++ b/apps/public/src/env.mjs
@@ -0,0 +1,62 @@
+import { createEnv } from '@t3-oss/env-nextjs';
+import { z } from 'zod';
+
+export const env = createEnv({
+ /**
+ * Specify your server-side environment variables schema here. This way you can ensure the app
+ * isn't built with invalid env vars.
+ */
+ server: {
+ DATABASE_URL: z
+ .string()
+ .url()
+ .refine(
+ (str) => !str.includes('YOUR_MYSQL_URL_HERE'),
+ 'You forgot to change the default URL'
+ ),
+ NODE_ENV: z
+ .enum(['development', 'test', 'production'])
+ .default('development'),
+ NEXTAUTH_SECRET:
+ process.env.NODE_ENV === 'production'
+ ? z.string()
+ : z.string().optional(),
+ NEXTAUTH_URL: z.preprocess(
+ // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
+ // Since NextAuth.js automatically uses the VERCEL_URL if present.
+ (str) => process.env.VERCEL_URL ?? str,
+ // VERCEL_URL doesn't include `https` so it cant be validated as a URL
+ process.env.VERCEL ? z.string() : z.string().url()
+ ),
+ },
+
+ /**
+ * Specify your client-side environment variables schema here. This way you can ensure the app
+ * isn't built with invalid env vars. To expose them to the client, prefix them with
+ * `NEXT_PUBLIC_`.
+ */
+ client: {
+ // NEXT_PUBLIC_CLIENTVAR: z.string(),
+ },
+
+ /**
+ * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
+ * middlewares) or client-side so we need to destruct manually.
+ */
+ runtimeEnv: {
+ DATABASE_URL: process.env.DATABASE_URL,
+ NODE_ENV: process.env.NODE_ENV,
+ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
+ NEXTAUTH_URL: process.env.NEXTAUTH_URL,
+ },
+ /**
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+ * useful for Docker builds.
+ */
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+ /**
+ * Makes it so that empty strings are treated as undefined.
+ * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.
+ */
+ emptyStringAsUndefined: true,
+});
diff --git a/apps/public/src/styles/globals.css b/apps/public/src/styles/globals.css
new file mode 100644
index 00000000..eabe4e32
--- /dev/null
+++ b/apps/public/src/styles/globals.css
@@ -0,0 +1,124 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+}
+
+.fancy-text {
+ @apply text-transparent inline-block bg-gradient-to-br from-blue-600 to-purple-900 bg-clip-text;
+}
+
+strong {
+ @apply font-bold;
+}
+
+.dashed {
+ --color: #fff;
+ background-image: repeating-linear-gradient(
+ to right,
+ var(--color) 0%,
+ var(--color) 50%,
+ transparent 50%,
+ transparent 100%
+ ),
+ repeating-linear-gradient(
+ to right,
+ var(--color) 0%,
+ var(--color) 50%,
+ transparent 50%,
+ transparent 100%
+ ),
+ repeating-linear-gradient(
+ to bottom,
+ var(--color) 0%,
+ var(--color) 50%,
+ transparent 50%,
+ transparent 100%
+ ),
+ repeating-linear-gradient(
+ to bottom,
+ var(--color) 0%,
+ var(--color) 50%,
+ transparent 50%,
+ transparent 100%
+ );
+ background-position:
+ left top,
+ left bottom,
+ left top,
+ right top;
+ background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
+ background-size:
+ 15px 2px,
+ 15px 2px,
+ 2px 15px,
+ 2px 15px;
+}
diff --git a/apps/public/src/utils/clipboard.ts b/apps/public/src/utils/clipboard.ts
new file mode 100644
index 00000000..d2b4d29e
--- /dev/null
+++ b/apps/public/src/utils/clipboard.ts
@@ -0,0 +1,9 @@
+import { toast } from '@/components/ui/use-toast';
+
+export function clipboard(value: string | number) {
+ navigator.clipboard.writeText(value.toString());
+ toast({
+ title: 'Copied to clipboard',
+ description: value.toString(),
+ });
+}
diff --git a/apps/public/src/utils/cn.ts b/apps/public/src/utils/cn.ts
new file mode 100644
index 00000000..04ec7b9b
--- /dev/null
+++ b/apps/public/src/utils/cn.ts
@@ -0,0 +1,7 @@
+import { clsx } from 'clsx';
+import type { ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/public/src/utils/constants.ts b/apps/public/src/utils/constants.ts
new file mode 100644
index 00000000..bebe213f
--- /dev/null
+++ b/apps/public/src/utils/constants.ts
@@ -0,0 +1,99 @@
+export const operators = {
+ is: 'Is',
+ isNot: 'Is not',
+ contains: 'Contains',
+ doesNotContain: 'Not contains',
+} as const;
+
+export const chartTypes = {
+ linear: 'Linear',
+ bar: 'Bar',
+ histogram: 'Histogram',
+ pie: 'Pie',
+ metric: 'Metric',
+ area: 'Area',
+ map: 'Map',
+} as const;
+
+export const lineTypes = {
+ monotone: 'Monotone',
+ monotoneX: 'Monotone X',
+ monotoneY: 'Monotone Y',
+ linear: 'Linear',
+ natural: 'Natural',
+ basis: 'Basis',
+ step: 'Step',
+ stepBefore: 'Step before',
+ stepAfter: 'Step after',
+ basisClosed: 'Basis closed',
+ basisOpen: 'Basis open',
+ bumpX: 'Bump X',
+ bumpY: 'Bump Y',
+ bump: 'Bump',
+ linearClosed: 'Linear closed',
+} as const;
+
+export const intervals = {
+ minute: 'minute',
+ day: 'day',
+ hour: 'hour',
+ month: 'month',
+} as const;
+
+export const alphabetIds = [
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H',
+ 'I',
+ 'J',
+] as const;
+
+export const timeRanges = {
+ '30min': '30min',
+ '1h': '1h',
+ today: 'today',
+ '24h': '24h',
+ '7d': '7d',
+ '14d': '14d',
+ '1m': '1m',
+ '3m': '3m',
+ '6m': '6m',
+ '1y': '1y',
+} as const;
+
+export const metrics = {
+ sum: 'sum',
+ average: 'average',
+ min: 'min',
+ max: 'max',
+} as const;
+
+export function isMinuteIntervalEnabledByRange(range: keyof typeof timeRanges) {
+ return range === '30min' || range === '1h';
+}
+
+export function isHourIntervalEnabledByRange(range: keyof typeof timeRanges) {
+ return (
+ isMinuteIntervalEnabledByRange(range) ||
+ range === 'today' ||
+ range === '24h'
+ );
+}
+
+export function getDefaultIntervalByRange(
+ range: keyof typeof timeRanges
+): keyof typeof intervals {
+ if (range === '30min' || range === '1h') {
+ return 'minute';
+ } else if (range === 'today' || range === '24h') {
+ return 'hour';
+ } else if (range === '7d' || range === '14d' || range === '1m') {
+ return 'day';
+ }
+ return 'month';
+}
diff --git a/apps/public/src/utils/date.ts b/apps/public/src/utils/date.ts
new file mode 100644
index 00000000..bf8179dc
--- /dev/null
+++ b/apps/public/src/utils/date.ts
@@ -0,0 +1,32 @@
+export function getDaysOldDate(days: number) {
+ const date = new Date();
+ date.setDate(date.getDate() - days);
+ return date;
+}
+
+export function dateDifferanceInDays(date1: Date, date2: Date) {
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+}
+
+export function getLocale() {
+ if (typeof navigator === 'undefined') {
+ return 'en-US';
+ }
+
+ return navigator.language ?? 'en-US';
+}
+
+export function formatDate(date: Date) {
+ return new Intl.DateTimeFormat(getLocale()).format(date);
+}
+
+export function formatDateTime(date: Date) {
+ return new Intl.DateTimeFormat(getLocale(), {
+ day: 'numeric',
+ month: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ }).format(date);
+}
diff --git a/apps/public/src/utils/getters.ts b/apps/public/src/utils/getters.ts
new file mode 100644
index 00000000..53d3e784
--- /dev/null
+++ b/apps/public/src/utils/getters.ts
@@ -0,0 +1,6 @@
+import type { Profile } from '@mixan/db';
+
+export function getProfileName(profile: Profile | undefined | null) {
+ if (!profile) return 'No profile';
+ return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
+}
diff --git a/apps/public/src/utils/math.ts b/apps/public/src/utils/math.ts
new file mode 100644
index 00000000..9360880c
--- /dev/null
+++ b/apps/public/src/utils/math.ts
@@ -0,0 +1,22 @@
+import { isNumber } from 'mathjs';
+
+export const round = (num: number, decimals = 2) => {
+ const factor = Math.pow(10, decimals);
+ return Math.round((num + Number.EPSILON) * factor) / factor;
+};
+
+export const average = (arr: (number | null)[]) => {
+ const filtered = arr.filter(isNumber);
+ return filtered.reduce((p, c) => p + c, 0) / filtered.length;
+};
+
+export const sum = (arr: (number | null)[]): number =>
+ round(arr.filter(isNumber).reduce((acc, item) => acc + item, 0));
+
+export const min = (arr: (number | null)[]): number =>
+ Math.min(...arr.filter(isNumber));
+
+export const max = (arr: (number | null)[]): number =>
+ Math.max(...arr.filter(isNumber));
+
+export const isFloat = (n: number) => n % 1 !== 0;
diff --git a/apps/public/src/utils/slug.ts b/apps/public/src/utils/slug.ts
new file mode 100644
index 00000000..2c448002
--- /dev/null
+++ b/apps/public/src/utils/slug.ts
@@ -0,0 +1,18 @@
+import _slugify from 'slugify';
+
+const slugify = (str: string) => {
+ return _slugify(
+ str
+ .replace('Γ₯', 'a')
+ .replace('Γ€', 'a')
+ .replace('ΓΆ', 'o')
+ .replace('Γ
', 'A')
+ .replace('Γ', 'A')
+ .replace('Γ', 'O'),
+ { lower: true, strict: true, trim: true }
+ );
+};
+
+export function slug(str: string): string {
+ return slugify(str);
+}
diff --git a/apps/public/src/utils/theme.ts b/apps/public/src/utils/theme.ts
new file mode 100644
index 00000000..6fccc0fa
--- /dev/null
+++ b/apps/public/src/utils/theme.ts
@@ -0,0 +1,17 @@
+import resolveConfig from 'tailwindcss/resolveConfig';
+
+import tailwinConfig from '../../tailwind.config';
+
+export const resolvedTailwindConfig = resolveConfig(tailwinConfig);
+
+export const theme = resolvedTailwindConfig.theme as Record;
+
+export function getChartColor(index: number): string {
+ const colors = theme?.colors ?? {};
+ const chartColors: string[] = Object.keys(colors)
+ .filter((key) => key.startsWith('chart-'))
+ .map((key) => colors[key])
+ .filter((item): item is string => typeof item === 'string');
+
+ return chartColors[index % chartColors.length]!;
+}
diff --git a/apps/public/src/utils/truncate.ts b/apps/public/src/utils/truncate.ts
new file mode 100644
index 00000000..9cecae76
--- /dev/null
+++ b/apps/public/src/utils/truncate.ts
@@ -0,0 +1,6 @@
+export function truncate(str: string, len: number) {
+ if (str.length <= len) {
+ return str;
+ }
+ return str.slice(0, len) + '...';
+}
diff --git a/apps/public/src/utils/validation.ts b/apps/public/src/utils/validation.ts
new file mode 100644
index 00000000..3b57c8bd
--- /dev/null
+++ b/apps/public/src/utils/validation.ts
@@ -0,0 +1,75 @@
+import { z } from 'zod';
+
+import {
+ chartTypes,
+ intervals,
+ lineTypes,
+ metrics,
+ operators,
+ timeRanges,
+} from './constants';
+
+export function objectToZodEnums(
+ obj: Record
+): [K, ...K[]] {
+ const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
+ return [firstKey!, ...otherKeys];
+}
+
+export const mapKeys = objectToZodEnums;
+
+export const zChartEvent = z.object({
+ id: z.string(),
+ name: z.string(),
+ displayName: z.string().optional(),
+ property: z.string().optional(),
+ segment: z.enum([
+ 'event',
+ 'user',
+ 'user_average',
+ 'one_event_per_user',
+ 'property_sum',
+ 'property_average',
+ ]),
+ filters: z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ operator: z.enum(objectToZodEnums(operators)),
+ value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())),
+ })
+ ),
+});
+export const zChartBreakdown = z.object({
+ id: z.string(),
+ name: z.string(),
+});
+
+export const zChartEvents = z.array(zChartEvent);
+export const zChartBreakdowns = z.array(zChartBreakdown);
+
+export const zChartType = z.enum(objectToZodEnums(chartTypes));
+
+export const zLineType = z.enum(objectToZodEnums(lineTypes));
+
+export const zTimeInterval = z.enum(objectToZodEnums(intervals));
+
+export const zMetric = z.enum(objectToZodEnums(metrics));
+
+export const zChartInput = z.object({
+ name: z.string(),
+ chartType: zChartType,
+ lineType: zLineType,
+ interval: zTimeInterval,
+ events: zChartEvents,
+ breakdowns: zChartBreakdowns,
+ range: z.enum(objectToZodEnums(timeRanges)),
+ previous: z.boolean(),
+ formula: z.string().optional(),
+ metric: zMetric,
+ unit: z.string().optional(),
+ previousIndicatorInverted: z.boolean().optional(),
+ projectId: z.string(),
+ startDate: z.string().nullish(),
+ endDate: z.string().nullish(),
+});
diff --git a/apps/public/tailwind.config.js b/apps/public/tailwind.config.js
new file mode 100644
index 00000000..fba13589
--- /dev/null
+++ b/apps/public/tailwind.config.js
@@ -0,0 +1,96 @@
+const colors = [
+ '#2563EB',
+ '#ff7557',
+ '#7fe1d8',
+ '#f8bc3c',
+ '#b3596e',
+ '#72bef4',
+ '#ffb27a',
+ '#0f7ea0',
+ '#3ba974',
+ '#febbb2',
+ '#cb80dc',
+ '#5cb7af',
+ '#7856ff',
+];
+
+/** @type {import('tailwindcss').Config} */
+const config = {
+ safelist: [...colors.map((color) => `chart-${color}`)],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ theme: {
+ extend: {
+ 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))',
+ },
+ ...colors.reduce((acc, color, index) => {
+ return {
+ ...acc,
+ [`chart-${index}`]: color,
+ };
+ }, {}),
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ boxShadow: {
+ DEFAULT: '0 5px 10px rgb(0 0 0 / 5%)',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: { height: '0px' },
+ to: { height: 'var(--radix-accordion-content-height)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height)' },
+ to: { height: '0px' },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate')],
+};
+
+export default config;
diff --git a/apps/public/tsconfig.json b/apps/public/tsconfig.json
new file mode 100644
index 00000000..9d21e044
--- /dev/null
+++ b/apps/public/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "@mixan/tsconfig/base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
+ "strictNullChecks": true
+ },
+ "include": [
+ ".",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/apps/web/src/components/Logo.tsx b/apps/web/src/components/Logo.tsx
index f5169729..02441844 100644
--- a/apps/web/src/components/Logo.tsx
+++ b/apps/web/src/components/Logo.tsx
@@ -9,7 +9,11 @@ export function Logo({ className }: LogoProps) {
-
+
openpanel.dev
);
diff --git a/captain-definition-public b/captain-definition-public
new file mode 100644
index 00000000..92d4674c
--- /dev/null
+++ b/captain-definition-public
@@ -0,0 +1,4 @@
+{
+ "schemaVersion": 2,
+ "dockerfilePath": "./apps/public/Dockerfile"
+}
diff --git a/packages/db/prisma/migrations/20240204201022_add_waitlist/migration.sql b/packages/db/prisma/migrations/20240204201022_add_waitlist/migration.sql
new file mode 100644
index 00000000..9a95282d
--- /dev/null
+++ b/packages/db/prisma/migrations/20240204201022_add_waitlist/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "waitlist" (
+ "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ "email" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "waitlist_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "waitlist_email_key" ON "waitlist"("email");
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index e36f4773..fd2ae773 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -211,3 +211,12 @@ model Invite {
@@map("invites")
}
+
+model Waitlist {
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ email String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+
+ @@map("waitlist")
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c5550d8b..0ae58c8e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,6 +24,166 @@ importers:
specifier: ^5.2.2
version: 5.2.2
+ apps/public:
+ dependencies:
+ '@mixan/db':
+ specifier: workspace:*
+ version: link:../../packages/db
+ '@radix-ui/react-alert-dialog':
+ specifier: ^1.0.5
+ version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-aspect-ratio':
+ specifier: ^1.0.3
+ version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-avatar':
+ specifier: ^1.0.4
+ version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.0.4
+ version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-dialog':
+ specifier: ^1.0.5
+ version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.0.6
+ version: 2.0.6(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-label':
+ specifier: ^2.0.2
+ version: 2.0.2(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-popover':
+ specifier: ^1.0.7
+ version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.0.5
+ version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-slot':
+ specifier: ^1.0.2
+ version: 1.0.2(@types/react@18.2.34)(react@18.2.0)
+ '@radix-ui/react-toast':
+ specifier: ^1.1.5
+ version: 1.1.5(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.0.7
+ version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
+ '@t3-oss/env-nextjs':
+ specifier: ^0.7.0
+ version: 0.7.1(typescript@5.2.2)(zod@3.22.4)
+ class-variance-authority:
+ specifier: ^0.7.0
+ version: 0.7.0
+ clsx:
+ specifier: ^2.0.0
+ version: 2.0.0
+ email-validator:
+ specifier: ^2.0.4
+ version: 2.0.4
+ embla-carousel-autoplay:
+ specifier: 8.0.0-rc22
+ version: 8.0.0-rc22(embla-carousel@8.0.0-rc22)
+ embla-carousel-react:
+ specifier: 8.0.0-rc22
+ version: 8.0.0-rc22(react@18.2.0)
+ hamburger-react:
+ specifier: ^2.5.0
+ version: 2.5.0(react@18.2.0)
+ lucide-react:
+ specifier: ^0.286.0
+ version: 0.286.0(react@18.2.0)
+ next:
+ specifier: ~14.0.4
+ version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
+ nuqs:
+ specifier: ^1.15.2
+ version: 1.15.2(next@14.0.4)
+ react:
+ specifier: 18.2.0
+ version: 18.2.0
+ react-animate-height:
+ specifier: ^3.2.3
+ version: 3.2.3(react-dom@18.2.0)(react@18.2.0)
+ react-dom:
+ specifier: 18.2.0
+ version: 18.2.0(react@18.2.0)
+ react-in-viewport:
+ specifier: 1.0.0-alpha.30
+ version: 1.0.0-alpha.30(react-dom@18.2.0)(react@18.2.0)
+ react-responsive:
+ specifier: ^9.0.2
+ version: 9.0.2(react@18.2.0)
+ react-syntax-highlighter:
+ specifier: ^15.5.0
+ version: 15.5.0(react@18.2.0)
+ tailwind-merge:
+ specifier: ^1.14.0
+ version: 1.14.0
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@3.3.5)
+ usehooks-ts:
+ specifier: ^2.9.1
+ version: 2.9.1(react-dom@18.2.0)(react@18.2.0)
+ zod:
+ specifier: ^3.22.4
+ version: 3.22.4
+ devDependencies:
+ '@mixan/eslint-config':
+ specifier: workspace:*
+ version: link:../../tooling/eslint
+ '@mixan/prettier-config':
+ specifier: workspace:*
+ version: link:../../tooling/prettier
+ '@mixan/tsconfig':
+ specifier: workspace:*
+ version: link:../../tooling/typescript
+ '@types/bcrypt':
+ specifier: ^5.0.0
+ version: 5.0.1
+ '@types/lodash.debounce':
+ specifier: ^4.0.9
+ version: 4.0.9
+ '@types/node':
+ specifier: ^18.16.0
+ version: 18.18.8
+ '@types/ramda':
+ specifier: ^0.29.6
+ version: 0.29.7
+ '@types/react':
+ specifier: ^18.2.20
+ version: 18.2.34
+ '@types/react-dom':
+ specifier: ^18.2.7
+ version: 18.2.14
+ '@types/react-syntax-highlighter':
+ specifier: ^15.5.9
+ version: 15.5.9
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.6.0
+ version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2)
+ '@typescript-eslint/parser':
+ specifier: ^6.6.0
+ version: 6.9.1(eslint@8.52.0)(typescript@5.2.2)
+ autoprefixer:
+ specifier: ^10.4.14
+ version: 10.4.16(postcss@8.4.31)
+ eslint:
+ specifier: ^8.48.0
+ version: 8.52.0
+ postcss:
+ specifier: ^8.4.27
+ version: 8.4.31
+ prettier:
+ specifier: ^3.0.3
+ version: 3.0.3
+ prettier-plugin-tailwindcss:
+ specifier: ^0.5.1
+ version: 0.5.6(prettier@3.0.3)
+ tailwindcss:
+ specifier: ^3.3.3
+ version: 3.3.5
+ typescript:
+ specifier: ^5.2.2
+ version: 5.2.2
+
apps/sdk-api:
dependencies:
'@fastify/cors':
@@ -1744,13 +1904,13 @@ packages:
/@radix-ui/primitive@1.0.0:
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
dev: false
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
dev: false
/@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0):
@@ -1792,7 +1952,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.34
'@types/react-dom': 18.2.14
@@ -1813,7 +1973,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.34
'@types/react-dom': 18.2.14
@@ -1834,7 +1994,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -1858,7 +2018,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -1886,7 +2046,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
@@ -1902,7 +2062,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
react: 18.2.0
dev: false
@@ -1915,7 +2075,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@types/react': 18.2.34
react: 18.2.0
dev: false
@@ -1925,7 +2085,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
react: 18.2.0
dev: false
@@ -1938,7 +2098,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@types/react': 18.2.34
react: 18.2.0
dev: false
@@ -1949,7 +2109,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
@@ -1983,7 +2143,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2024,7 +2184,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
@@ -2047,7 +2207,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
@@ -2072,7 +2232,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2091,7 +2251,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
react: 18.2.0
dev: false
@@ -2104,7 +2264,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@types/react': 18.2.34
react: 18.2.0
dev: false
@@ -2115,7 +2275,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
@@ -2136,7 +2296,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2151,7 +2311,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
@@ -2165,7 +2325,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@types/react': 18.2.34
react: 18.2.0
@@ -2184,7 +2344,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.34
'@types/react-dom': 18.2.14
@@ -2205,7 +2365,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2243,7 +2403,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2278,7 +2438,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2301,7 +2461,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -2320,7 +2480,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.34
'@types/react-dom': 18.2.14
@@ -2334,7 +2494,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
react: 18.2.0
@@ -2369,7 +2529,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -2388,7 +2548,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.34)(react@18.2.0)
'@types/react': 18.2.34
'@types/react-dom': 18.2.14
@@ -2409,7 +2569,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2438,7 +2598,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/number': 1.0.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2459,7 +2619,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
@@ -2473,7 +2633,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@types/react': 18.2.34
react: 18.2.0
@@ -2492,7 +2652,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0)
@@ -2548,7 +2708,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
react: 18.2.0
dev: false
@@ -2571,7 +2731,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
@@ -2596,7 +2756,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
@@ -2610,7 +2770,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0)
'@types/react': 18.2.34
react: 18.2.0
@@ -2621,7 +2781,7 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
react: 18.2.0
dev: false
@@ -2662,7 +2822,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.34
react: 18.2.0
@@ -2707,7 +2867,7 @@ packages:
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
dev: false
/@reduxjs/toolkit@1.9.7(react-redux@8.1.3)(react@18.2.0):
@@ -3437,7 +3597,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.22.1
- caniuse-lite: 1.0.30001559
+ caniuse-lite: 1.0.30001577
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -3537,7 +3697,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
- caniuse-lite: 1.0.30001559
+ caniuse-lite: 1.0.30001577
electron-to-chromium: 1.4.574
node-releases: 2.0.13
update-browserslist-db: 1.0.13(browserslist@4.22.1)
@@ -3610,10 +3770,10 @@ packages:
/caniuse-lite@1.0.30001559:
resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==}
+ dev: false
/caniuse-lite@1.0.30001577:
resolution: {integrity: sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==}
- dev: false
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@@ -4059,6 +4219,41 @@ packages:
/electron-to-chromium@1.4.574:
resolution: {integrity: sha512-bg1m8L0n02xRzx4LsTTMbBPiUd9yIR+74iPtS/Ao65CuXvhVZHP0ym1kSdDG3yHFDXqHQQBKujlN1AQ8qZnyFg==}
+ /email-validator@2.0.4:
+ resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==}
+ engines: {node: '>4.0'}
+ dev: false
+
+ /embla-carousel-autoplay@8.0.0-rc22(embla-carousel@8.0.0-rc22):
+ resolution: {integrity: sha512-UFR9ocKapxuYwcAOv8mb6Rmy7TENpzzHTymKADzB1L5dAJJxjUtOci/OpE3KrZedQaniLMz3HIO9hHqgj1h/3w==}
+ peerDependencies:
+ embla-carousel: 8.0.0-rc22
+ dependencies:
+ embla-carousel: 8.0.0-rc22
+ dev: false
+
+ /embla-carousel-react@8.0.0-rc22(react@18.2.0):
+ resolution: {integrity: sha512-NwmISV0Cw9XVo76Vquz3hJaeZe7qoCRtrzStxlEY7qfZD8WR/f4JlQAso35URTs1BeYVhcuClflelioo+Zmidg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.1 || ^18.0.0
+ dependencies:
+ embla-carousel: 8.0.0-rc22
+ embla-carousel-reactive-utils: 8.0.0-rc22(embla-carousel@8.0.0-rc22)
+ react: 18.2.0
+ dev: false
+
+ /embla-carousel-reactive-utils@8.0.0-rc22(embla-carousel@8.0.0-rc22):
+ resolution: {integrity: sha512-K4b8QhQGXYW5wr4l+U6XryhafsFV5c/IyohDnZN3MGoYIB9xY7qpjUWAcs5bTDjAD+qCZPOuXre0D3IVa28mqw==}
+ peerDependencies:
+ embla-carousel: 8.0.0-rc22
+ dependencies:
+ embla-carousel: 8.0.0-rc22
+ dev: false
+
+ /embla-carousel@8.0.0-rc22:
+ resolution: {integrity: sha512-MeXnPT1LShfgAu8qXj3CskayV0R6OkHx7w3cPTx+Q5ZWKyShKpIuu7qVQJ5BoFegalE4n6yxqoQaRuGFbK9pYw==}
+ dev: false
+
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
@@ -6651,7 +6846,7 @@ packages:
peerDependencies:
react: '>= 0.14.0'
dependencies:
- '@babel/runtime': 7.23.2
+ '@babel/runtime': 7.23.9
highlight.js: 10.7.3
lowlight: 1.20.0
prismjs: 1.29.0