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

This commit is contained in:
Turbobot
2025-10-09 17:18:09 -04:00
committed by Gabriel Garcia
parent 7325fddd45
commit 1d1d28da75
177 changed files with 4640 additions and 14190 deletions

170
.gitignore vendored
View File

@@ -1,140 +1,38 @@
# Logs # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
logs
*.log # Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Misc
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json .DS_Store
*.pem
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
**/.helix

0
.npmrc Normal file
View File

135
README.md Normal file
View File

@@ -0,0 +1,135 @@
# Turborepo starter
This Turborepo starter is maintained by the Turborepo core team.
## Using this example
Run the following command:
```sh
npx create-turbo@latest
```
## What's inside?
This Turborepo includes the following packages/apps:
### Apps and Packages
- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
### Utilities
This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo build
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo build
yarn dlx turbo build
pnpm exec turbo build
```
You can build a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters):
```
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo build --filter=docs
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo build --filter=docs
yarn exec turbo build --filter=docs
pnpm exec turbo build --filter=docs
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo dev
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo dev
yarn exec turbo dev
pnpm exec turbo dev
```
You can develop a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters):
```
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo dev --filter=web
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo dev --filter=web
yarn exec turbo dev --filter=web
pnpm exec turbo dev --filter=web
```
### Remote Caching
> [!TIP]
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
```
cd my-turborepo
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo login
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo login
yarn exec turbo login
pnpm exec turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
turbo link
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
npx turbo link
yarn exec turbo link
pnpm exec turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks)
- [Caching](https://turborepo.com/docs/crafting-your-repository/caching)
- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching)
- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters)
- [Configuration Options](https://turborepo.com/docs/reference/configuration)
- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference)

36
apps/docs/.gitignore vendored Normal file
View File

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

36
apps/docs/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

BIN
apps/docs/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

50
apps/docs/app/globals.css Normal file
View File

@@ -0,0 +1,50 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
.imgDark {
display: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
.imgLight {
display: none;
}
.imgDark {
display: unset;
}
}

31
apps/docs/app/layout.tsx Normal file
View File

@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,188 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-synthesis: none;
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 180px;
}
button.secondary {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
background: transparent;
border-color: var(--gray-alpha-200);
min-width: 180px;
}
.footer {
font-family: var(--font-geist-sans);
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}

102
apps/docs/app/page.tsx Normal file
View File

@@ -0,0 +1,102 @@
import Image, { type ImageProps } from "next/image";
import { Button } from "@repo/ui/button";
import styles from "./page.module.css";
type Props = Omit<ImageProps, "src"> & {
srcLight: string;
srcDark: string;
};
const ThemeImage = (props: Props) => {
const { srcLight, srcDark, ...rest } = props;
return (
<>
<Image {...rest} src={srcLight} className="imgLight" />
<Image {...rest} src={srcDark} className="imgDark" />
</>
);
};
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<ThemeImage
className={styles.logo}
srcLight="turborepo-dark.svg"
srcDark="turborepo-light.svg"
alt="Turborepo logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>apps/docs/app/page.tsx</code>
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new/clone?demo-description=Learn+to+implement+a+monorepo+with+a+two+Next.js+sites+that+has+installed+three+local+packages.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4K8ZISWAzJ8X1504ca0zmC%2F0b21a1c6246add355e55816278ef54bc%2FBasic.png&demo-title=Monorepo+with+Turborepo&demo-url=https%3A%2F%2Fexamples-basic-web.vercel.sh%2F&from=templates&project-name=Monorepo+with+Turborepo&repository-name=monorepo-turborepo&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fturborepo%2Ftree%2Fmain%2Fexamples%2Fbasic&root-directory=apps%2Fdocs&skippable-integrations=1&teamSlug=vercel&utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://turborepo.com/docs?utm_source"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
<Button appName="docs" className={styles.secondary}>
Open alert
</Button>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com/templates?search=turborepo&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://turborepo.com?utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to turborepo.com
</a>
</footer>
</div>
);
}

View File

@@ -0,0 +1,4 @@
import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default nextJsConfig;

4
apps/docs/next.config.js Normal file
View File

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

28
apps/docs/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "docs",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3001",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@repo/ui": "workspace:*",
"next": "^15.5.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.34.0",
"typescript": "5.9.2"
}
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 13.5V6.5V5.41421C14.5 5.149 14.3946 4.89464 14.2071 4.70711L9.79289 0.292893C9.60536 0.105357 9.351 0 9.08579 0H8H3H1.5V1.5V13.5C1.5 14.8807 2.61929 16 4 16H12C13.3807 16 14.5 14.8807 14.5 13.5ZM13 13.5V6.5H9.5H8V5V1.5H3V13.5C3 14.0523 3.44772 14.5 4 14.5H12C12.5523 14.5 13 14.0523 13 13.5ZM9.5 5V2.12132L12.3787 5H9.5ZM5.13 5.00062H4.505V6.25062H5.13H6H6.625V5.00062H6H5.13ZM4.505 8H5.13H11H11.625V9.25H11H5.13H4.505V8ZM5.13 11H4.505V12.25H5.13H11H11.625V11H11H5.13Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_868_525)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z" fill="#666666"/>
</g>
<defs>
<clipPath id="clip0_868_525">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,19 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6565V22.3773H91.0977V30.6565H106.16V58.1875H115.935V30.6565H130.998Z" fill="black"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2275V22.3773H162.768V41.2799C162.768 47.0155 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0155 144.315 41.2799V22.3773H134.539V42.2275C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="black"/>
<path d="M187.508 46.3173H197.234L204.914 58.1875H216.136L207.458 45.2699C212.346 43.5243 215.338 39.634 215.338 34.3473C215.338 26.6665 209.603 22.3773 200.874 22.3773H177.732V58.1875H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.053 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="black"/>
<path d="M219.887 58.1875H245.472C253.452 58.1875 258.041 54.397 258.041 48.0629C258.041 43.8235 255.348 40.9308 252.156 39.634C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1875ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0205 248.615 46.9657C248.615 48.9108 247.168 50.2075 244.525 50.2075H229.263V43.7238Z" fill="black"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.786 281.942 58.786C294.461 58.786 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2575C288.525 30.2575 293.463 34.1478 293.463 40.2824C293.463 46.417 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.417 270.421 40.2824C270.421 34.1478 275.359 30.2575 281.942 30.2575Z" fill="black"/>
<path d="M317.526 46.3173H327.251L334.932 58.1875H346.154L337.476 45.2699C342.364 43.5243 345.356 39.634 345.356 34.3473C345.356 26.6665 339.62 22.3773 330.892 22.3773H307.75V58.1875H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.053 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="black"/>
<path d="M349.904 22.3773V58.1875H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6565H384.717V22.3773H349.904Z" fill="black"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1875H399.204V46.7662ZM399.204 38.6365V30.5568H411.673C415.164 30.5568 417.059 32.053 417.059 34.5967C417.059 37.0904 415.164 38.6365 411.673 38.6365H399.204Z" fill="black"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.786 450.948 58.786C463.467 58.786 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2575C457.532 30.2575 462.469 34.1478 462.469 40.2824C462.469 46.417 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.417 439.427 40.2824C439.427 34.1478 444.365 30.2575 450.948 30.2575Z" fill="black"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_278)"/>
<defs>
<linearGradient id="paint0_linear_2028_278" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,19 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6566V22.3773H91.0977V30.6566H106.16V58.1876H115.935V30.6566H130.998Z" fill="white"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2276V22.3773H162.768V41.2799C162.768 47.0156 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0156 144.315 41.2799V22.3773H134.539V42.2276C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="white"/>
<path d="M187.508 46.3173H197.234L204.914 58.1876H216.136L207.458 45.2699C212.346 43.5243 215.338 39.6341 215.338 34.3473C215.338 26.6666 209.603 22.3773 200.874 22.3773H177.732V58.1876H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.0531 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="white"/>
<path d="M219.887 58.1876H245.472C253.452 58.1876 258.041 54.3971 258.041 48.0629C258.041 43.8236 255.348 40.9308 252.156 39.6341C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1876ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0206 248.615 46.9657C248.615 48.9108 247.168 50.2076 244.525 50.2076H229.263V43.7238Z" fill="white"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.7861 281.942 58.7861C294.461 58.7861 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2576C288.525 30.2576 293.463 34.1478 293.463 40.2824C293.463 46.4171 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.4171 270.421 40.2824C270.421 34.1478 275.359 30.2576 281.942 30.2576Z" fill="white"/>
<path d="M317.526 46.3173H327.251L334.932 58.1876H346.154L337.476 45.2699C342.364 43.5243 345.356 39.6341 345.356 34.3473C345.356 26.6666 339.62 22.3773 330.892 22.3773H307.75V58.1876H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.0531 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="white"/>
<path d="M349.904 22.3773V58.1876H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6566H384.717V22.3773H349.904Z" fill="white"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1876H399.204V46.7662ZM399.204 38.6366V30.5568H411.673C415.164 30.5568 417.059 32.0531 417.059 34.5967C417.059 37.0904 415.164 38.6366 411.673 38.6366H399.204Z" fill="white"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.7861 450.948 58.7861C463.467 58.7861 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2576C457.532 30.2576 462.469 34.1478 462.469 40.2824C462.469 46.4171 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.4171 439.427 40.2824C439.427 34.1478 444.365 30.2576 450.948 30.2576Z" fill="white"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_477)"/>
<defs>
<linearGradient id="paint0_linear_2028_477" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,10 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_977_547)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3L18.5 17H2.5L10.5 3Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_977_547">
<rect width="16" height="16" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM3.75 5.5C4.16421 5.5 4.5 5.16421 4.5 4.75C4.5 4.33579 4.16421 4 3.75 4C3.33579 4 3 4.33579 3 4.75C3 5.16421 3.33579 5.5 3.75 5.5ZM7 4.75C7 5.16421 6.66421 5.5 6.25 5.5C5.83579 5.5 5.5 5.16421 5.5 4.75C5.5 4.33579 5.83579 4 6.25 4C6.66421 4 7 4.33579 7 4.75ZM8.75 5.5C9.16421 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.16421 4 8.75 4C8.33579 4 8 4.33579 8 4.75C8 5.16421 8.33579 5.5 8.75 5.5Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

20
apps/docs/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
"next.config.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

36
apps/web/.gitignore vendored Normal file
View File

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

36
apps/web/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

BIN
apps/web/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

50
apps/web/app/globals.css Normal file
View File

@@ -0,0 +1,50 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
.imgDark {
display: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
.imgLight {
display: none;
}
.imgDark {
display: unset;
}
}

31
apps/web/app/layout.tsx Normal file
View File

@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,188 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-synthesis: none;
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 180px;
}
button.secondary {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
font-family: var(--font-geist-sans);
border: 1px solid transparent;
transition: background 0.2s, color 0.2s, border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
background: transparent;
border-color: var(--gray-alpha-200);
min-width: 180px;
}
.footer {
font-family: var(--font-geist-sans);
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}

102
apps/web/app/page.tsx Normal file
View File

@@ -0,0 +1,102 @@
import Image, { type ImageProps } from "next/image";
import { Button } from "@repo/ui/button";
import styles from "./page.module.css";
type Props = Omit<ImageProps, "src"> & {
srcLight: string;
srcDark: string;
};
const ThemeImage = (props: Props) => {
const { srcLight, srcDark, ...rest } = props;
return (
<>
<Image {...rest} src={srcLight} className="imgLight" />
<Image {...rest} src={srcDark} className="imgDark" />
</>
);
};
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<ThemeImage
className={styles.logo}
srcLight="turborepo-dark.svg"
srcDark="turborepo-light.svg"
alt="Turborepo logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>apps/web/app/page.tsx</code>
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new/clone?demo-description=Learn+to+implement+a+monorepo+with+a+two+Next.js+sites+that+has+installed+three+local+packages.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4K8ZISWAzJ8X1504ca0zmC%2F0b21a1c6246add355e55816278ef54bc%2FBasic.png&demo-title=Monorepo+with+Turborepo&demo-url=https%3A%2F%2Fexamples-basic-web.vercel.sh%2F&from=templates&project-name=Monorepo+with+Turborepo&repository-name=monorepo-turborepo&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fturborepo%2Ftree%2Fmain%2Fexamples%2Fbasic&root-directory=apps%2Fdocs&skippable-integrations=1&teamSlug=vercel&utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://turborepo.com/docs?utm_source"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
<Button appName="web" className={styles.secondary}>
Open alert
</Button>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com/templates?search=turborepo&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://turborepo.com?utm_source=create-turbo"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to turborepo.com
</a>
</footer>
</div>
);
}

View File

@@ -0,0 +1,4 @@
import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default nextJsConfig;

4
apps/web/next.config.js Normal file
View File

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

28
apps/web/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "web",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3000",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@repo/ui": "workspace:*",
"next": "^15.5.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.34.0",
"typescript": "5.9.2"
}
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 13.5V6.5V5.41421C14.5 5.149 14.3946 4.89464 14.2071 4.70711L9.79289 0.292893C9.60536 0.105357 9.351 0 9.08579 0H8H3H1.5V1.5V13.5C1.5 14.8807 2.61929 16 4 16H12C13.3807 16 14.5 14.8807 14.5 13.5ZM13 13.5V6.5H9.5H8V5V1.5H3V13.5C3 14.0523 3.44772 14.5 4 14.5H12C12.5523 14.5 13 14.0523 13 13.5ZM9.5 5V2.12132L12.3787 5H9.5ZM5.13 5.00062H4.505V6.25062H5.13H6H6.625V5.00062H6H5.13ZM4.505 8H5.13H11H11.625V9.25H11H5.13H4.505V8ZM5.13 11H4.505V12.25H5.13H11H11.625V11H11H5.13Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 645 B

10
apps/web/public/globe.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_868_525)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z" fill="#666666"/>
</g>
<defs>
<clipPath id="clip0_868_525">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

1
apps/web/public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,19 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6565V22.3773H91.0977V30.6565H106.16V58.1875H115.935V30.6565H130.998Z" fill="black"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2275V22.3773H162.768V41.2799C162.768 47.0155 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0155 144.315 41.2799V22.3773H134.539V42.2275C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="black"/>
<path d="M187.508 46.3173H197.234L204.914 58.1875H216.136L207.458 45.2699C212.346 43.5243 215.338 39.634 215.338 34.3473C215.338 26.6665 209.603 22.3773 200.874 22.3773H177.732V58.1875H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.053 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="black"/>
<path d="M219.887 58.1875H245.472C253.452 58.1875 258.041 54.397 258.041 48.0629C258.041 43.8235 255.348 40.9308 252.156 39.634C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1875ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0205 248.615 46.9657C248.615 48.9108 247.168 50.2075 244.525 50.2075H229.263V43.7238Z" fill="black"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.786 281.942 58.786C294.461 58.786 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2575C288.525 30.2575 293.463 34.1478 293.463 40.2824C293.463 46.417 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.417 270.421 40.2824C270.421 34.1478 275.359 30.2575 281.942 30.2575Z" fill="black"/>
<path d="M317.526 46.3173H327.251L334.932 58.1875H346.154L337.476 45.2699C342.364 43.5243 345.356 39.634 345.356 34.3473C345.356 26.6665 339.62 22.3773 330.892 22.3773H307.75V58.1875H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.053 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="black"/>
<path d="M349.904 22.3773V58.1875H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6565H384.717V22.3773H349.904Z" fill="black"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1875H399.204V46.7662ZM399.204 38.6365V30.5568H411.673C415.164 30.5568 417.059 32.053 417.059 34.5967C417.059 37.0904 415.164 38.6365 411.673 38.6365H399.204Z" fill="black"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.786 450.948 58.786C463.467 58.786 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2575C457.532 30.2575 462.469 34.1478 462.469 40.2824C462.469 46.417 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.417 439.427 40.2824C439.427 34.1478 444.365 30.2575 450.948 30.2575Z" fill="black"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_278)"/>
<defs>
<linearGradient id="paint0_linear_2028_278" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,19 @@
<svg width="473" height="76" viewBox="0 0 473 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.998 30.6566V22.3773H91.0977V30.6566H106.16V58.1876H115.935V30.6566H130.998Z" fill="white"/>
<path d="M153.542 58.7362C165.811 58.7362 172.544 52.5018 172.544 42.2276V22.3773H162.768V41.2799C162.768 47.0156 159.776 50.2574 153.542 50.2574C147.307 50.2574 144.315 47.0156 144.315 41.2799V22.3773H134.539V42.2276C134.539 52.5018 141.272 58.7362 153.542 58.7362Z" fill="white"/>
<path d="M187.508 46.3173H197.234L204.914 58.1876H216.136L207.458 45.2699C212.346 43.5243 215.338 39.6341 215.338 34.3473C215.338 26.6666 209.603 22.3773 200.874 22.3773H177.732V58.1876H187.508V46.3173ZM187.508 38.5867V30.5568H200.376C203.817 30.5568 205.712 32.0531 205.712 34.5967C205.712 36.9907 203.817 38.5867 200.376 38.5867H187.508Z" fill="white"/>
<path d="M219.887 58.1876H245.472C253.452 58.1876 258.041 54.3971 258.041 48.0629C258.041 43.8236 255.348 40.9308 252.156 39.6341C254.35 38.5867 257.043 36.0929 257.043 32.1528C257.043 25.8187 252.555 22.3773 244.625 22.3773H219.887V58.1876ZM229.263 36.3922V30.3074H243.627C246.32 30.3074 247.817 31.3548 247.817 33.3498C247.817 35.3448 246.32 36.3922 243.627 36.3922H229.263ZM229.263 43.7238H244.525C247.168 43.7238 248.615 45.0206 248.615 46.9657C248.615 48.9108 247.168 50.2076 244.525 50.2076H229.263V43.7238Z" fill="white"/>
<path d="M281.942 21.7788C269.423 21.7788 260.396 29.6092 260.396 40.2824C260.396 50.9557 269.423 58.7861 281.942 58.7861C294.461 58.7861 303.438 50.9557 303.438 40.2824C303.438 29.6092 294.461 21.7788 281.942 21.7788ZM281.942 30.2576C288.525 30.2576 293.463 34.1478 293.463 40.2824C293.463 46.4171 288.525 50.3073 281.942 50.3073C275.359 50.3073 270.421 46.4171 270.421 40.2824C270.421 34.1478 275.359 30.2576 281.942 30.2576Z" fill="white"/>
<path d="M317.526 46.3173H327.251L334.932 58.1876H346.154L337.476 45.2699C342.364 43.5243 345.356 39.6341 345.356 34.3473C345.356 26.6666 339.62 22.3773 330.892 22.3773H307.75V58.1876H317.526V46.3173ZM317.526 38.5867V30.5568H330.394C333.835 30.5568 335.73 32.0531 335.73 34.5967C335.73 36.9907 333.835 38.5867 330.394 38.5867H317.526Z" fill="white"/>
<path d="M349.904 22.3773V58.1876H384.717V49.9083H359.48V44.0729H381.874V35.9932H359.48V30.6566H384.717V22.3773H349.904Z" fill="white"/>
<path d="M399.204 46.7662H412.221C420.95 46.7662 426.685 42.5767 426.685 34.5967C426.685 26.5668 420.95 22.3773 412.221 22.3773H389.428V58.1876H399.204V46.7662ZM399.204 38.6366V30.5568H411.673C415.164 30.5568 417.059 32.0531 417.059 34.5967C417.059 37.0904 415.164 38.6366 411.673 38.6366H399.204Z" fill="white"/>
<path d="M450.948 21.7788C438.43 21.7788 429.402 29.6092 429.402 40.2824C429.402 50.9557 438.43 58.7861 450.948 58.7861C463.467 58.7861 472.444 50.9557 472.444 40.2824C472.444 29.6092 463.467 21.7788 450.948 21.7788ZM450.948 30.2576C457.532 30.2576 462.469 34.1478 462.469 40.2824C462.469 46.4171 457.532 50.3073 450.948 50.3073C444.365 50.3073 439.427 46.4171 439.427 40.2824C439.427 34.1478 444.365 30.2576 450.948 30.2576Z" fill="white"/>
<path d="M38.5017 18.0956C27.2499 18.0956 18.0957 27.2498 18.0957 38.5016C18.0957 49.7534 27.2499 58.9076 38.5017 58.9076C49.7535 58.9076 58.9077 49.7534 58.9077 38.5016C58.9077 27.2498 49.7535 18.0956 38.5017 18.0956ZM38.5017 49.0618C32.6687 49.0618 27.9415 44.3346 27.9415 38.5016C27.9415 32.6686 32.6687 27.9414 38.5017 27.9414C44.3347 27.9414 49.0619 32.6686 49.0619 38.5016C49.0619 44.3346 44.3347 49.0618 38.5017 49.0618Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2115 14.744V7.125C56.7719 8.0104 69.9275 21.7208 69.9275 38.5016C69.9275 55.2824 56.7719 68.989 40.2115 69.8782V62.2592C52.5539 61.3776 62.3275 51.0644 62.3275 38.5016C62.3275 25.9388 52.5539 15.6256 40.2115 14.744ZM20.5048 54.0815C17.233 50.3043 15.124 45.4935 14.7478 40.2115H7.125C7.5202 47.6025 10.4766 54.3095 15.1088 59.4737L20.501 54.0815H20.5048ZM36.7916 69.8782V62.2592C31.5058 61.883 26.695 59.7778 22.9178 56.5022L17.5256 61.8944C22.6936 66.5304 29.4006 69.483 36.7878 69.8782H36.7916Z" fill="url(#paint0_linear_2028_477)"/>
<defs>
<linearGradient id="paint0_linear_2028_477" x1="41.443" y1="11.5372" x2="10.5567" y2="42.4236" gradientUnits="userSpaceOnUse">
<stop stop-color="#0096FF"/>
<stop offset="1" stop-color="#FF1E56"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,10 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_977_547)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3L18.5 17H2.5L10.5 3Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_977_547">
<rect width="16" height="16" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM3.75 5.5C4.16421 5.5 4.5 5.16421 4.5 4.75C4.5 4.33579 4.16421 4 3.75 4C3.33579 4 3 4.33579 3 4.75C3 5.16421 3.33579 5.5 3.75 5.5ZM7 4.75C7 5.16421 6.66421 5.5 6.25 5.5C5.83579 5.5 5.5 5.16421 5.5 4.75C5.5 4.33579 5.83579 4 6.25 4C6.66421 4 7 4.33579 7 4.75ZM8.75 5.5C9.16421 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.16421 4 8.75 4C8.33579 4 8 4.33579 8 4.75C8 5.16421 8.33579 5.5 8.75 5.5Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

20
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
"next.config.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@@ -1,26 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dist-electron
*.local
release/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,30 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -1,43 +0,0 @@
// @see - https://www.electron.build/configuration/configuration
{
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
"appId": "YourAppID",
"asar": true,
"productName": "YourAppName",
"directories": {
"output": "release/${version}"
},
"files": [
"dist",
"dist-electron"
],
"mac": {
"target": [
"dmg"
],
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
},
"linux": {
"target": [
"AppImage"
],
"artifactName": "${productName}-Linux-${version}.${ext}"
}
}

View File

@@ -1,27 +0,0 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
/**
* The built directory structure
*
* ```tree
* ├─┬─┬ dist
* │ │ └── index.html
* │ │
* │ ├─┬ dist-electron
* │ │ ├── main.js
* │ │ └── preload.js
* │
* ```
*/
APP_ROOT: string
/** /dist/ or /public/ */
VITE_PUBLIC: string
}
}
// Used in Renderer process, expose in `preload.ts`
interface Window {
ipcRenderer: import('electron').IpcRenderer
}

View File

@@ -1,70 +0,0 @@
import { app, BrowserWindow } from "electron";
// import { createRequire } from 'node:module'
import { fileURLToPath } from "node:url";
import path from "node:path";
// const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// The built directory structure
//
// ├─┬─┬ dist
// │ │ └── index.html
// │ │
// │ ├─┬ dist-electron
// │ │ ├── main.js
// │ │ └── preload.mjs
// │
process.env.APP_ROOT = path.join(__dirname, "..");
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, "public")
: RENDERER_DIST;
let win: BrowserWindow | null;
function createWindow() {
win = new BrowserWindow({
icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
},
});
// Test active push message to Renderer-process.
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL);
} else {
// win.loadFile('dist/index.html')
win.loadFile(path.join(RENDERER_DIST, "index.html"));
}
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
win = null;
}
});
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.whenReady().then(createWindow);

View File

@@ -1,24 +0,0 @@
import { ipcRenderer, contextBridge } from 'electron'
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', {
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args
return ipcRenderer.off(channel, ...omit)
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args
return ipcRenderer.send(channel, ...omit)
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args
return ipcRenderer.invoke(channel, ...omit)
},
// You can expose other APTs you need here.
// ...
})

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,66 +0,0 @@
{
"name": "concord-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev:web": "VITE_APP_MODE=web bunx --bun vite",
"dev:electron": "bunx --bun vite --open",
"build": "tsc && bunx --bun vite build && electron-builder",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "bunx --bun vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.544.0",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-router": "^7.9.3",
"react-syntax-highlighter": "^15.6.6",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/bun": "^1.2.23",
"@types/react": "^18.3.26",
"@types/react-dom": "^18.3.7",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^4.7.0",
"electron": "^30.5.1",
"electron-builder": "^24.13.3",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.23",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^5.4.20",
"vite-plugin-electron": "^0.28.8",
"vite-plugin-electron-renderer": "^0.14.6"
},
"main": "dist-electron/main.js"
}

View File

@@ -1,34 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.9202 127.84C99.2223 127.84 127.84 99.2223 127.84 63.9202C127.84 28.6181 99.2223 0 63.9202 0C28.6181 0 0 28.6181 0 63.9202C0 99.2223 28.6181 127.84 63.9202 127.84Z" fill="url(#paint0_linear_103_2)"/>
<g id="not-lightning" clip-path="url(#clip0_103_2)">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 64 64"
to="360 64 64"
dur="20s"
repeatCount="indefinite"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M63.9835 27.6986C66.4621 27.6986 68.4714 25.6861 68.4714 23.2036C68.4714 20.7211 66.4621 18.7086 63.9835 18.7086C61.5049 18.7086 59.4956 20.7211 59.4956 23.2036C59.4956 25.6861 61.5049 27.6986 63.9835 27.6986Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
</g>
<path d="M70.7175 48.0096L56.3133 50.676C56.0766 50.7199 55.9013 50.9094 55.887 51.1369L55.001 65.2742C54.9801 65.6072 55.3038 65.8656 55.6478 65.7907L59.6582 64.9163C60.0334 64.8346 60.3724 65.1468 60.2953 65.5033L59.1038 71.0151C59.0237 71.386 59.3923 71.7032 59.7758 71.5932L62.2528 70.8822C62.6368 70.7721 63.0057 71.0902 62.9245 71.4615L61.031 80.1193C60.9126 80.6608 61.6751 80.9561 61.9931 80.4918L62.2055 80.1817L73.9428 58.053C74.1393 57.6825 73.8004 57.26 73.3696 57.3385L69.2417 58.0912C68.8538 58.1618 68.5237 57.8206 68.6332 57.462L71.3274 48.6385C71.437 48.2794 71.1058 47.9378 70.7175 48.0096Z" fill="url(#paint1_linear_103_2)"/>
<defs>
<linearGradient id="paint0_linear_103_2" x1="1.43824" y1="7.91009" x2="56.3296" y2="82.4569" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear_103_2" x1="60.3173" y1="48.7336" x2="64.237" y2="77.1962" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
<clipPath id="clip0_103_2">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,26 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_103_2)">
<path d="M63.9202 127.84C99.2223 127.84 127.84 99.2223 127.84 63.9202C127.84 28.6181 99.2223 0 63.9202 0C28.6181 0 0 28.6181 0 63.9202C0 99.2223 28.6181 127.84 63.9202 127.84Z" fill="url(#paint0_linear_103_2)"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M63.9835 27.6986C66.4621 27.6986 68.4714 25.6861 68.4714 23.2036C68.4714 20.7211 66.4621 18.7086 63.9835 18.7086C61.5049 18.7086 59.4956 20.7211 59.4956 23.2036C59.4956 25.6861 61.5049 27.6986 63.9835 27.6986Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M70.7175 48.0096L56.3133 50.676C56.0766 50.7199 55.9013 50.9094 55.887 51.1369L55.001 65.2742C54.9801 65.6072 55.3038 65.8656 55.6478 65.7907L59.6582 64.9163C60.0334 64.8346 60.3724 65.1468 60.2953 65.5033L59.1038 71.0151C59.0237 71.386 59.3923 71.7032 59.7758 71.5932L62.2528 70.8822C62.6368 70.7721 63.0057 71.0902 62.9245 71.4615L61.031 80.1193C60.9126 80.6608 61.6751 80.9561 61.9931 80.4918L62.2055 80.1817L73.9428 58.053C74.1393 57.6825 73.8004 57.26 73.3696 57.3385L69.2417 58.0912C68.8538 58.1618 68.5237 57.8206 68.6332 57.462L71.3274 48.6385C71.437 48.2794 71.1058 47.9378 70.7175 48.0096Z" fill="url(#paint1_linear_103_2)"/>
</g>
<defs>
<linearGradient id="paint0_linear_103_2" x1="1.43824" y1="7.91009" x2="56.3296" y2="82.4569" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear_103_2" x1="60.3173" y1="48.7336" x2="64.237" y2="77.1962" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
<clipPath id="clip0_103_2">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,114 +0,0 @@
import React, { useEffect } from "react";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from "@/components/theme-provider";
import AppLayout from "@/components/layout/AppLayout";
import LoginPage from "@/pages/LoginPage";
import ChatPage from "@/pages/ChatPage";
import SettingsPage from "@/pages/SettingsPage";
import NotFoundPage from "@/pages/NotFoundPage";
import { useVoiceStore } from "@/stores/voiceStore";
import { queryClient } from "@/lib/api-client";
import { useAuthStore } from "@/stores/authStore";
import ErrorBoundary from "@/components/common/ErrorBoundary";
import { Home } from "lucide-react";
import { Socket } from "socket.io-client";
// Protected Route wrapper
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { isAuthenticated } = useAuthStore();
// Enable this when you want to enforce authentication
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
// Home page component - shows server selection
const HomePage: React.FC = () => {
return (
<div className="flex-1 flex items-center justify-center bg-concord-primary">
<div className="text-center text-concord-secondary max-w-md">
<div className="w-16 h-16 mx-auto mb-4 bg-concord-secondary rounded-full flex items-center justify-center">
<Home />
</div>
<h2 className="text-xl font-semibold mb-2 text-concord-primary">
Welcome to Concord
</h2>
<p className="text-sm mb-4">
Select a server from the sidebar to start chatting, or create a new
server
</p>
</div>
</div>
);
};
function App(props: { socket: Socket }) {
const initVoiceStore = useVoiceStore((state) => state.init);
useEffect(() => {
initVoiceStore(props.socket);
return () => {
useVoiceStore.getState().cleanup();
};
}, [props.socket, initVoiceStore]);
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="concord-theme">
<Router>
<div className="h-screen w-screen overflow-hidden bg-background text-foreground">
<Routes>
{/* Auth routes */}
<Route path="/login" element={<LoginPage />} />
{/* Protected routes with layout */}
<Route
path="/"
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
>
{/* Default redirect to home */}
<Route index element={<HomePage />} />
{/* Server and channel routes */}
<Route path="channels/:instanceId" element={<ChatPage />} />
<Route
path="channels/:instanceId/:channelId"
element={<ChatPage />}
/>
{/* Settings */}
<Route path="settings" element={<SettingsPage />} />
<Route path="settings/:section" element={<SettingsPage />} />
</Route>
{/* 404 */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
</Router>
{import.meta.env.DEV === true && <ReactQueryDevtools />}
{/* Toast notifications */}
<Toaster />
</ThemeProvider>
</QueryClientProvider>
</ErrorBoundary>
);
}
export default App;

View File

@@ -1,13 +0,0 @@
# TODO
- Messages
- Channels should include messages
- sample data
- message components
- User
- Set up fake user with auth to:
- Confirm userpanel is ok
- test login flow
- Add server ui
- Add channel ui
- Role based for above ^

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,103 +0,0 @@
import React from "react";
import { Hash, Volume2 } from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { Channel } from "@/lib/api-client";
import { useVoiceStore } from "@/stores/voiceStore";
import { useInstanceMembers } from "@/hooks/useServers";
import { useAuthStore } from "@/stores/authStore";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
interface ChannelItemProps {
channel: Channel;
}
const ChannelItem: React.FC<ChannelItemProps> = ({ channel }) => {
const { instanceId, channelId: activeChannelId } = useParams();
const navigate = useNavigate();
// Voice store hooks
const {
joinChannel,
leaveChannel,
activeVoiceChannelId,
remoteStreams,
localStream,
} = useVoiceStore();
// Data hooks
const { data: members } = useInstanceMembers(instanceId);
const { user: currentUser, token } = useAuthStore(); // Get token from auth store
const isConnectedToThisChannel = activeVoiceChannelId === channel.id;
const isActive = activeChannelId === channel.id;
const handleChannelClick = () => {
if (channel.type === "text") {
navigate(`/channels/${instanceId}/${channel.id}`);
} else if (channel.type === "voice") {
if (isConnectedToThisChannel) {
leaveChannel();
} else if (currentUser && token) {
joinChannel(channel.id, currentUser.id, token);
}
}
};
const Icon = channel.type === "voice" ? Volume2 : Hash;
const connectedUserIds = Array.from(remoteStreams.keys());
return (
<div className={`${isActive ?? "visible"}`}>
<button
onClick={handleChannelClick}
className={`w-full flex items-center p-1.5 rounded-md text-left transition-colors ${
isActive || isConnectedToThisChannel
? "bg-concord-secondary text-concord-primary"
: "text-concord-secondary hover:bg-concord-secondary/50 hover:text-concord-primary"
}`}
>
<Icon className="w-5 h-5 mr-2 flex-shrink-0" />
<span className="truncate flex-1">{channel.name}</span>
</button>
{/* Render connected users for this voice channel */}
{isConnectedToThisChannel && (
<div className="pl-4 mt-1 space-y-1">
{/* Current User */}
{localStream && currentUser && (
<div className="flex items-center p-1">
<Avatar className="h-6 w-6">
<AvatarImage src={currentUser.picture || ""} />
<AvatarFallback>
{currentUser.username.slice(0, 2)}
</AvatarFallback>
</Avatar>
<span className="ml-2 text-sm text-concord-primary">
{currentUser.nickname || currentUser.username}
</span>
</div>
)}
{/* Remote Users */}
{connectedUserIds.map((userId) => {
const member = members?.find((m) => m.id === userId);
if (!member) return null;
return (
<div key={userId} className="flex items-center p-1">
<Avatar className="h-6 w-6">
<AvatarImage src={member.picture || ""} />
<AvatarFallback>{member.username.slice(0, 2)}</AvatarFallback>
</Avatar>
<span className="ml-2 text-sm text-concord-primary">
{member.nickname || member.username}
</span>
</div>
);
})}
</div>
)}
</div>
);
};
export default ChannelItem;

View File

@@ -1,102 +0,0 @@
import React, { useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CategoryWithChannels } from "@/types/api";
import ChannelItem from "@/components/channel/ChannelItem";
interface CategoryHeaderProps {
category: CategoryWithChannels;
isExpanded: boolean;
onToggle: () => void;
}
const CategoryHeader: React.FC<CategoryHeaderProps> = ({
category,
isExpanded,
onToggle,
}) => {
return (
<Button
variant="ghost"
className="w-full justify-between p-4 h-6 text-md text-concord-primary font-semibold interactive-hover uppercase tracking-wide group"
onClick={() => {
onToggle();
}}
>
<div className="flex items-center">
{isExpanded ? (
<ChevronDown size={12} className="mr-1" />
) : (
<ChevronRight size={12} className="mr-1" />
)}
<span className="truncate">{category.name}</span>
</div>
</Button>
);
};
interface ChannelListProps {
categories: CategoryWithChannels[];
}
const ChannelList: React.FC<ChannelListProps> = ({ categories }) => {
// Track expanded categories
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set(categories.map((cat) => cat.id)), // Start with all expanded
);
const toggleCategory = (categoryId: string) => {
setExpandedCategories((prev) => {
const newSet = new Set(prev);
if (newSet.has(categoryId)) {
newSet.delete(categoryId);
} else {
newSet.add(categoryId);
}
return newSet;
});
};
if (!categories || categories.length === 0) {
return (
<div className="text-sm text-gray-400 px-2 py-4 text-center">
No channels available
</div>
);
}
return (
<div className="space-y-1">
{categories
.sort((a, b) => a.position - b.position)
.map((category) => {
const isExpanded = expandedCategories.has(category.id);
return (
<div key={category.id} className="space-y-0.5">
{/* Category Header */}
<CategoryHeader
category={category}
isExpanded={isExpanded}
onToggle={() => toggleCategory(category.id)}
/>
<div
className={`ml-2 space-y-0.5 transition-all duration-300 ease-in-out overflow-hidden ${
isExpanded ? "max-h-screen opacity-100" : "max-h-0 opacity-0"
}`}
>
{category.channels
.sort((a, b) => a.position - b.position)
.map((channel) => (
<ChannelItem key={channel.id} channel={channel} />
))}
</div>
</div>
);
})}
</div>
);
};
export default ChannelList;

View File

@@ -1,137 +0,0 @@
import React from "react";
import {
Avatar as ShadcnAvatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { User } from "@/types/database";
import { cn } from "@/lib/utils";
interface AvatarProps {
user: Pick<User, "id" | "username" | "nickname" | "picture" | "status">;
size?: "xs" | "sm" | "md" | "lg" | "xl";
showStatus?: boolean;
className?: string;
onClick?: () => void;
}
const sizeClasses = {
xs: "h-6 w-6",
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-12 w-12",
xl: "h-16 w-16",
};
const statusSizes = {
xs: "w-2 h-2",
sm: "w-3 h-3",
md: "w-3 h-3",
lg: "w-4 h-4",
xl: "w-5 h-5",
};
const statusPositions = {
xs: "-bottom-0.5 -right-0.5",
sm: "-bottom-0.5 -right-0.5",
md: "-bottom-0.5 -right-0.5",
lg: "-bottom-1 -right-1",
xl: "-bottom-1 -right-1",
};
const Avatar: React.FC<AvatarProps> = ({
user,
size = "md",
showStatus = false,
className,
onClick,
}) => {
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "bg-status-online";
case "away":
return "bg-status-away";
case "busy":
return "bg-status-busy";
case "offline":
default:
return "bg-status-offline";
}
};
const getUserInitials = (username: string, nickname: string | null) => {
const name = nickname || username;
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
const getFallbackColor = (userId: string) => {
// Generate a consistent color based on user ID using theme colors
const colors = [
"bg-red-500",
"bg-blue-500",
"bg-green-500",
"bg-yellow-500",
"bg-purple-500",
"bg-pink-500",
"bg-indigo-500",
"bg-teal-500",
];
const hash = userId.split("").reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);
return colors[Math.abs(hash) % colors.length];
};
return (
<div className="relative inline-block">
<ShadcnAvatar
className={cn(
sizeClasses[size],
onClick && "cursor-pointer hover:opacity-80 transition-opacity",
className,
)}
onClick={onClick}
>
<AvatarImage
src={user.picture || undefined}
alt={user.nickname || user.username}
/>
<AvatarFallback
className={cn(
"text-white font-medium",
getFallbackColor(user.id),
size === "xs" && "text-xs",
size === "sm" && "text-xs",
size === "md" && "text-sm",
size === "lg" && "text-base",
size === "xl" && "text-lg",
)}
>
{getUserInitials(user.username, user.nickname ? user.nickname : null)}
</AvatarFallback>
</ShadcnAvatar>
{showStatus && (
<div
className={cn(
"absolute rounded-full border-2 border-sidebar",
statusSizes[size],
statusPositions[size],
getStatusColor(user.status),
)}
/>
)}
</div>
);
};
export default Avatar;

View File

@@ -1,120 +0,0 @@
import { Component, ErrorInfo, ReactNode } from "react";
import { AlertTriangle, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card } from "../ui/card";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
errorInfo?: ErrorInfo;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo);
this.setState({
error,
errorInfo,
});
}
private handleReload = () => {
window.location.reload();
};
private handleReset = () => {
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
};
public render() {
if (this.state.hasError) {
// Custom fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen bg-concord-primary flex items-center justify-center p-4">
<Card className="max-w-md bg-concord-secondary border-concord w-full space-y-4">
<Alert className="border-red-500 bg-red-950/50">
<AlertTriangle className="h-4 w-4 text-red-500" />
<AlertTitle className="text-red-400">
Something went wrong
</AlertTitle>
<AlertDescription className="text-gray-300">
The application encountered an unexpected error. This might be a
temporary issue.
</AlertDescription>
</Alert>
<div className="space-y-2">
<Button
onClick={this.handleReset}
className="w-full"
variant="outline"
>
<RotateCcw size={16} className="mr-2" />
Try Again
</Button>
<Button
onClick={this.handleReload}
variant="secondary"
className="w-full"
>
Reload Application
</Button>
</div>
{/* Error details in development */}
{process.env.NODE_ENV === "development" && this.state.error && (
<details className="mt-4 p-3 bg-concord-secondary rounded-lg text-sm">
<summary className="cursor-pointer text-red-400 font-medium mb-2">
Error Details (Development)
</summary>
<div className="space-y-2 text-gray-300">
<div>
<strong>Error:</strong> {this.state.error.message}
</div>
<div>
<strong>Stack:</strong>
<pre className="mt-1 text-xs overflow-auto text-gray-400">
{this.state.error.stack}
</pre>
</div>
{this.state.errorInfo && (
<div>
<strong>Component Stack:</strong>
<pre className="mt-1 text-xs overflow-auto text-gray-400">
{this.state.errorInfo.componentStack}
</pre>
</div>
)}
</div>
</details>
)}
</Card>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,41 +0,0 @@
import React from "react";
import { cn } from "@/lib/utils";
interface LoadingSpinnerProps {
size?: "xs" | "sm" | "md" | "lg" | "xl";
className?: string;
color?: "white" | "blue" | "gray";
}
const sizeClasses = {
xs: "h-3 w-3",
sm: "h-4 w-4",
md: "h-6 w-6",
lg: "h-8 w-8",
xl: "h-12 w-12",
};
const colorClasses = {
white: "border-white",
blue: "border-blue-500",
gray: "border-gray-400",
};
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
size = "md",
className,
color = "white",
}) => {
return (
<div
className={cn(
"animate-spin rounded-full border-2 border-transparent border-t-current",
sizeClasses[size],
colorClasses[color],
className,
)}
/>
);
};
export default LoadingSpinner;

View File

@@ -1,79 +0,0 @@
import React, { useEffect } from "react";
import { Outlet, useLocation } from "react-router";
import { useAuthStore } from "@/stores/authStore";
import { useUiStore } from "@/stores/uiStore";
import ServerSidebar from "@/components/layout/ServerSidebar";
import ChannelSidebar from "@/components/layout/ChannelSidebar";
import UserPanel from "@/components/layout/UserPanel";
import MemberList from "@/components/layout/MemberList";
import LoadingSpinner from "@/components/common/LoadingSpinner";
import VoiceConnectionManager from "@/components/voice/VoiceConnectionManager";
const AppLayout: React.FC = () => {
const { isLoading } = useAuthStore();
const {
showMemberList,
sidebarCollapsed,
shouldShowChannelSidebar,
updateSidebarVisibility,
} = useUiStore();
const location = useLocation();
// Update sidebar visibility when route changes
useEffect(() => {
updateSidebarVisibility(location.pathname);
}, [location.pathname, updateSidebarVisibility]);
if (isLoading) {
return (
<div className="h-screen w-screen flex items-center justify-center bg-concord-primary">
<LoadingSpinner size="lg" />
</div>
);
}
return (
<div className="flex h-screen overflow-y-auto bg-concord-primary text-concord-primary">
{/* This component handles playing audio from remote users */}
<VoiceConnectionManager />
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
<div className="relative min-w-1/16 sidebar-primary flex-shrink-0">
<ServerSidebar />
</div>
{/* Channel Sidebar - Only shown when in a server context and not collapsed */}
{shouldShowChannelSidebar && (
<div
className={`${
sidebarCollapsed
? "w-0" // Collapse by setting width to 0
: "w-60" // Default width
}
flex-col flex-shrink-0 sidebar-secondary transition-all duration-200 ease-in-out overflow-hidden`}
>
<div className="flex flex-col h-full">
<ChannelSidebar />
<UserPanel />
</div>
</div>
)}
{/* Main Content Area */}
<div
className={`flex-1 flex flex-col min-w-0 ${
!sidebarCollapsed ? "" : ""
} transition-all duration-200 ease-in-out bg-concord-secondary`}
>
<Outlet />
</div>
{/* Member List - Only shown when in a channel and member list is enabled */}
{showMemberList && shouldShowChannelSidebar && (
<div className="flex-0 min-w-1/7 sidebar-secondary order-l border-sidebar">
<MemberList />
</div>
)}
</div>
);
};
export default AppLayout;

View File

@@ -1,99 +0,0 @@
import React from "react";
import { useParams } from "react-router";
import { ChevronDown, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useInstanceDetails } from "@/hooks/useServers";
import { useUiStore } from "@/stores/uiStore";
import ChannelList from "@/components/channel/ChannelList";
import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
const ChannelSidebar: React.FC = () => {
const { instanceId } = useParams();
const { data: instance, isLoading: instanceLoading } =
useInstanceDetails(instanceId);
const categories = instance?.categories;
const {
showCreateChannel,
closeCreateChannel,
openCreateChannel,
openServerSettings,
} = useUiStore();
// Only show for valid instance IDs
if (!instanceId) {
return null;
}
if (instanceLoading) {
return (
<div className="sidebar-secondary flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (!instance) {
return (
<div className="sidebar-secondary flex items-center justify-center h-full">
<div className="text-concord-secondary">Server not found</div>
</div>
);
}
return (
<div className="sidebar-secondary flex-1">
<ScrollArea className="">
{/* Server Header */}
<div className="flex items-center justify-between border-b border-concord-primary shadow-sm px-6 py-4">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<Button
variant="ghost"
className="flex items-center justify-between w-full h-8 font-semibold text-concord-primary text-xl hover:bg-concord-tertiary"
onClick={openServerSettings}
>
<span className="truncate">{instance.name}</span>
<ChevronDown size={20} className="flex-shrink-0 ml-1" />
</Button>
</div>
</div>
{/* Channel Categories and Channels */}
<ScrollArea className="flex-1">
<div className="p-2">
{categories && categories.length > 0 ? (
<ChannelList categories={categories} />
) : (
<div className="text-sm text-concord-secondary text-center px-2 py-4">
No channels yet
</div>
)}
</div>
</ScrollArea>
{/* Bottom Actions */}
<div className="border-t border-sidebar px-2 py-2">
<div className="flex items-center">
<Button
variant="ghost"
size="sm"
className="justify-start interactive-hover flex-grow-1"
onClick={openCreateChannel}
>
<Plus size={16} className="mr-1" />
Add Channel
</Button>
</div>
</div>
</ScrollArea>
<CreateChannelModal
isOpen={showCreateChannel}
onClose={closeCreateChannel}
categories={categories}
instanceId={instance.id}
/>
</div>
);
};
export default ChannelSidebar;

View File

@@ -1,265 +0,0 @@
import React from "react";
import { useParams } from "react-router";
import { Crown, Shield, UserIcon } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Role, User } from "@/types/database";
import { useInstanceMembers } from "@/hooks/useServers";
import { useAuthStore } from "@/stores/authStore";
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "bg-status-online";
case "away":
return "bg-status-away";
case "busy":
return "bg-status-busy";
default:
return "bg-status-offline";
}
};
interface MemberItemProps {
member: User;
instanceId: string;
isOwner?: boolean;
currentUserRolePriority: number;
}
const getUserRoleForInstance = (roles: Role[], instanceId: string): string => {
if (!instanceId) return "member";
const roleEntry = roles.find((r) => r.instanceId === instanceId);
return roleEntry?.role || "member";
};
const getRoleInfo = (role: string) => {
const lowerRole = role.toLowerCase();
switch (lowerRole) {
case "admin":
return { color: "#ff6b6b", priority: 3, name: "Admin" };
case "mod":
return { color: "#4ecdc4", priority: 2, name: "Moderator" };
case "member":
return { color: null, priority: 1, name: "Member" };
default:
return {
color: null,
priority: 0,
name: role.charAt(0).toUpperCase() + role.slice(1),
};
}
};
const MemberItem: React.FC<MemberItemProps> = ({
member,
instanceId,
isOwner = false,
currentUserRolePriority,
}) => {
// Determine the role for this specific instance
const userRole = getUserRoleForInstance(member.roles, instanceId);
const roleInfo = getRoleInfo(userRole);
const memberRolePriority = roleInfo.priority;
// Consider if this member is a global admin as well
const isGlobalAdmin = member.admin || false;
let effectiveRoleInfo = roleInfo;
let effectiveMemberRolePriority = memberRolePriority;
if (isGlobalAdmin && roleInfo.priority < 3) {
effectiveRoleInfo = getRoleInfo("admin");
effectiveMemberRolePriority = 3;
}
return (
<Button
variant="ghost"
className="w-full justify-start p-2 h-auto hover:bg-concord-tertiary/50"
// disable if the current member is an admin
disabled={currentUserRolePriority < 3 || effectiveMemberRolePriority >= 3}
>
<div className="flex items-center gap-3 w-full">
<div className="relative">
<Avatar className="h-8 w-8">
<AvatarImage
src={member.picture || undefined}
alt={member.username}
/>
<AvatarFallback className="text-xs bg-primary text-primary-foreground">
{member.username?.slice(0, 2).toUpperCase() || "???"}
</AvatarFallback>
</Avatar>
{/* Status indicator */}
<div
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-sidebar ${getStatusColor(member.status)}`}
/>
</div>
<div className="flex-1 min-w-0 text-left">
<div className="flex items-center gap-1">
{isOwner && (
<Crown size={12} className="text-yellow-500 flex-shrink-0" />
)}
{/* Display Shield for Admins and Mods, not for Members */}
{!isOwner && effectiveMemberRolePriority > 1 && (
<Shield
size={12}
className="flex-shrink-0"
style={{
color: effectiveRoleInfo.color || "var(--background)",
}}
/>
)}
<span
className="text-sm font-medium truncate"
style={{
color: effectiveRoleInfo.color || "var(--color-text-primary)",
}}
>
{member.nickname || member.username}
</span>
</div>
{member.bio && (
<div className="text-xs text-concord-secondary truncate">
{member.bio}
</div>
)}
</div>
</div>
</Button>
);
};
const MemberList: React.FC = () => {
const { instanceId } = useParams<{ instanceId: string }>();
const { data: members, isLoading } = useInstanceMembers(instanceId);
const { user: currentUser } = useAuthStore();
const currentUserRoleInfo = React.useMemo(() => {
if (!currentUser || !instanceId) {
return { role: "member", priority: 1, name: "Member", color: null };
}
// If the current user is a global admin, they are effectively an admin of any instance.
if (currentUser.admin) {
return { role: "admin", priority: 3, name: "Admin", color: "#ff6b6b" };
}
const role = getUserRoleForInstance(currentUser.roles, instanceId);
return { ...getRoleInfo(role), role: role };
}, [currentUser, instanceId]);
if (!instanceId) {
return null;
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
</div>
);
}
if (!members || members.length === 0) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-concord-secondary text-sm">No members found</div>
</div>
);
}
// Group members by their role for the current instance.
const groupedMembers = members.reduce(
(acc, member) => {
// Determine the effective role for this instance.
let effectiveRoleName = getUserRoleForInstance(
member.roles as Role[],
instanceId,
);
// Global admin is instance admin
if (member.admin && effectiveRoleName !== "admin") {
effectiveRoleName = "admin";
}
const roleInfo = getRoleInfo(effectiveRoleName);
if (!acc[roleInfo.name]) {
acc[roleInfo.name] = [];
}
acc[roleInfo.name].push(member as User);
return acc;
},
{} as Record<string, User[]>,
);
// Get all unique role names present and sort them by priority.
const sortedRoleNames = Object.keys(groupedMembers).sort(
(roleNameA, roleNameB) => {
const priorityA = getRoleInfo(roleNameA).priority;
const priorityB = getRoleInfo(roleNameB).priority;
return priorityB - priorityA;
},
);
return (
<div className="flex flex-col flex-grow-1 w-full border-l border-concord h-full sidebar-secondary">
{/* Header */}
<div className="px-6 py-4 pb-5 border-b border-concord flex items-center justify-between">
<div className="flex items-center space-x-2">
<UserIcon size={20} className="h-5 w-5 text-concord-secondary" />
<p className="font-semibold text-xl text-concord-primary tracking-wide">
Members
</p>
</div>
<p className="font-medium text-concord-secondary tracking-wide">
{members.length}
</p>
</div>
{/* Member List */}
<ScrollArea className="flex-1">
<div className="py-2">
{sortedRoleNames.map((roleName) => {
const roleMembers = groupedMembers[roleName];
// Sort members within each role group alphabetically by username.
const sortedMembers = roleMembers.sort((a, b) =>
(a.nickname || a.username).localeCompare(
b.nickname || b.username,
),
);
return (
<div key={roleName} className="mb-4">
{/* Role Header */}
<div className="px-4 py-1">
<h4 className="text-xs font-semibold text-concord-secondary uppercase tracking-wide">
{roleName} {roleMembers.length}
</h4>
</div>
{/* Role Members */}
<div className="space-y-1">
{sortedMembers.map((member) => (
<MemberItem
key={member.id}
member={member}
instanceId={instanceId}
currentUserRolePriority={currentUserRoleInfo.priority}
isOwner={false}
/>
))}
</div>
</div>
);
})}
</div>
</ScrollArea>
</div>
);
};
export default MemberList;

View File

@@ -1,141 +0,0 @@
import React from "react";
import { useNavigate, useParams } from "react-router";
import { Plus, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useServers } from "@/hooks/useServers";
import { useUiStore } from "@/stores/uiStore";
import { useAuthStore } from "@/stores/authStore";
import ServerIcon from "@/components/server/ServerIcon";
import { getAccessibleInstances, isGlobalAdmin } from "@/utils/permissions";
const ServerSidebar: React.FC = () => {
const navigate = useNavigate();
const { instanceId } = useParams();
const { data: allServers = [], isLoading } = useServers();
const { openCreateServer, setActiveInstance, getSelectedChannelForInstance } =
useUiStore();
const { user: currentUser } = useAuthStore();
// Filter servers based on user permissions
const accessibleServers = getAccessibleInstances(currentUser, allServers);
const canCreateServer = isGlobalAdmin(currentUser);
const handleServerClick = (serverId: string) => {
setActiveInstance(serverId);
const lastChannelId = getSelectedChannelForInstance(serverId);
if (lastChannelId) {
navigate(`/channels/${serverId}/${lastChannelId}`);
} else {
// Fallback: navigate to the server, let the page component handle finding a channel
navigate(`/channels/${serverId}`);
}
};
const handleHomeClick = () => {
setActiveInstance(null);
navigate("/");
};
const handleCreateServer = () => {
if (canCreateServer) {
openCreateServer();
}
};
return (
<TooltipProvider>
<div className="sidebar-primary flex flex-col items-center h-full space-y-2 w-full">
{/* Home/DM Button */}
<Tooltip key={"home-server"}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={`w-12 h-12 mt-2 transition-all duration-200 ${
!instanceId || instanceId === "@me"
? "rounded-xl border-primary bg-primary/10 border-2"
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
}`}
onClick={handleHomeClick}
>
<Home size={4} />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>{isGlobalAdmin(currentUser) ? "Admin Dashboard" : "Home"}</p>
</TooltipContent>
</Tooltip>
{/* Separator */}
<div className="w-full h-0.5 bg-border rounded-full" />{" "}
{/* Server List */}
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
{isLoading ? (
<div className="flex items-center justify-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
</div>
) : accessibleServers.length > 0 ? (
accessibleServers.map((server) => (
<Tooltip key={server.id}>
<TooltipTrigger asChild>
<div>
<ServerIcon
server={server}
isActive={instanceId === server.id}
onClick={() => handleServerClick(server.id)}
/>
</div>
</TooltipTrigger>
<TooltipContent side="right">
<p>{server.name}</p>
</TooltipContent>
</Tooltip>
))
) : currentUser ? (
<div className="text-center py-4 px-2">
<div className="text-xs text-concord-secondary mb-2">
No servers available
</div>
{canCreateServer && (
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={handleCreateServer}
>
Create One
</Button>
)}
</div>
) : null}
{/* Add Server Button - Only show if user can create servers */}
{canCreateServer && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="w-12 mb-4 h-12 rounded-2xl hover:rounded-xl bg-concord-secondary hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
onClick={handleCreateServer}
>
<Plus size={24} />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Add a Server</p>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</TooltipProvider>
);
};
export default ServerSidebar;

View File

@@ -1,179 +0,0 @@
import React from "react";
import { Settings, Mic, MicOff, Headphones } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAuthStore } from "@/stores/authStore";
import { useVoiceStore } from "@/stores/voiceStore";
import { useNavigate } from "react-router";
// Status color utility
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "bg-status-online";
case "away":
return "bg-status-away";
case "busy":
return "bg-status-busy";
default:
return "bg-status-offline";
}
};
// Voice Controls Component
interface VoiceControlsProps {
isMuted: boolean;
isDeafened: boolean;
onMuteToggle: () => void;
onDeafenToggle: () => void;
}
const VoiceControls: React.FC<VoiceControlsProps> = ({
isMuted,
isDeafened,
onMuteToggle,
onDeafenToggle,
}) => {
return (
<div className="flex items-center space-x-1">
<TooltipProvider>
{/* Mute/Unmute */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={`h-8 w-8 ${isMuted ? "text-destructive hover:text-destructive/80" : "interactive-hover"}`}
onClick={onMuteToggle}
>
{isMuted ? <MicOff size={18} /> : <Mic size={18} />}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isMuted ? "Unmute" : "Mute"}</p>
</TooltipContent>
</Tooltip>
{/* Deafen/Undeafen */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={`h-8 w-8 ${isDeafened ? "text-destructive hover:text-destructive/80" : "interactive-hover"}`}
onClick={onDeafenToggle}
>
<Headphones size={18} />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isDeafened ? "Undeafen" : "Deafen"}</p>
</TooltipContent>
</Tooltip>
{/* Settings */}
</TooltipProvider>
</div>
);
};
// User Avatar Component
interface UserAvatarProps {
user: any;
size?: "sm" | "md" | "lg";
showStatus?: boolean;
}
const UserAvatar: React.FC<UserAvatarProps> = ({
user,
size = "md",
showStatus = true,
}) => {
const sizeClasses = {
sm: "h-6 w-6",
md: "h-8 w-8",
lg: "h-10 w-10",
};
const statusSizeClasses = {
sm: "w-2 h-2",
md: "w-3 h-3",
lg: "w-4 h-4",
};
return (
<div className="relative">
<Avatar className={sizeClasses[size]}>
<AvatarImage src={user.picture || undefined} alt={user.username} />
<AvatarFallback className="text-xs text-primary-foreground bg-primary">
{user.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
{showStatus && (
<div
className={`absolute -bottom-0.5 -right-0.5 ${statusSizeClasses[size]} rounded-full border-2 border-sidebar ${getStatusColor(user.status)}`}
/>
)}
</div>
);
};
const UserPanel: React.FC = () => {
const { user } = useAuthStore();
const navigate = useNavigate();
const { isConnected, isMuted, isDeafened, toggleMute, toggleDeafen } =
useVoiceStore();
return (
<div className="user-panel flex items-center p-3 bg-concord-tertiary border-t border-sidebar min-h-16 rounded-xl m-2">
{/* User Info */}
<UserAvatar user={user} size="md" />
<div className="ml-2 flex-1 min-w-0 text-left">
<div className="text-sm font-medium text-concord-primary truncate">
{user?.nickname || user?.username}
</div>
<div className="text-xs text-concord-secondary truncate capitalize">
{user?.status}
</div>
</div>
{/* Voice Controls */}
{isConnected && (
<VoiceControls
isMuted={isMuted}
isDeafened={isDeafened}
onMuteToggle={toggleMute}
onDeafenToggle={toggleDeafen}
/>
)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 interactive-hover"
onClick={() => navigate("/settings")}
>
<Settings size={18} />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>User Settings</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
};
export default UserPanel;

View File

@@ -1,55 +0,0 @@
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Copy, Reply, MoreHorizontal } from "lucide-react";
import { Message } from "@/lib/api-client";
interface MessageActionsModalProps {
message: Message;
// isOwnMessage?: boolean;
onReply?: (messageId: string) => void;
}
export const MessageActionsDropdown: React.FC<MessageActionsModalProps> = ({
message,
onReply,
// isOwnMessage,
}) => {
const handleReply = () => {
onReply?.(message.id);
};
const handleCopyText = () => {
navigator.clipboard.writeText(message.text);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem onSelect={handleReply}>
<Reply className="h-4 w-4 mr-2" />
<span>Reply</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleCopyText}>
<Copy className="h-4 w-4 mr-2" />
<span>Copy Text</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -1,244 +0,0 @@
import { Copy, Reply, Pin } from "lucide-react";
import { Button } from "@/components/ui/button";
import ReactMarkdown from "react-markdown";
import { formatDistanceToNow, isValid, parseISO } from "date-fns";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import SyntaxHighlighter from "react-syntax-highlighter";
import {
dark,
solarizedLight,
} from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useTheme } from "@/components/theme-provider";
import { Message } from "@/lib/api-client";
import { useState } from "react";
// User type for message component
interface MessageUser {
id: string;
username?: string;
nickname?: string | null;
picture?: string | null;
}
// Message Props interface
interface MessageProps {
message: Message;
user: MessageUser;
replyTo?: Message;
replyToUser?: MessageUser;
onReply?: (messageId: string) => void;
}
export const MessageComponent: React.FC<MessageProps> = ({
message,
user,
replyTo,
replyToUser,
onReply,
}) => {
const [isHovered, setIsHovered] = useState(false);
const formatTimestamp = (timestamp: string) => {
try {
// First try parsing as ISO string
let date = parseISO(timestamp);
// If that fails, try regular Date constructor
if (!isValid(date)) {
date = new Date(timestamp);
}
// Final check if date is valid
if (!isValid(date)) {
console.error("Invalid timestamp:", timestamp);
return "Invalid date";
}
return formatDistanceToNow(date, { addSuffix: true });
} catch (error) {
console.error("Error formatting timestamp:", timestamp, error);
return "Invalid date";
}
};
// const isOwnMessage = currentUser?.id === message.userId;
const { mode } = useTheme();
// Get username with fallback
const username = user.username || user.username || "Unknown User";
const displayName = user.nickname || user.nickname || username;
const isDeleted = message.deleted;
if (isDeleted) {
return (
<div className="px-4 py-2 opacity-50">
<div className="flex gap-3">
<div className="w-10 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm text-concord-secondary italic border border-border rounded px-3 py-2 bg-concord-tertiary/50">
This message has been deleted
</div>
</div>
</div>
</div>
);
}
return (
<>
<div
className="group relative px-4 py-2 hover:bg-concord-secondary/50 transition-colors"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex gap-3">
{/* Avatar - always show */}
<div className="w-10 flex-shrink-0">
<Avatar className="h-10 w-10">
<AvatarImage src={user.picture || undefined} alt={username} />
<AvatarFallback className="text-sm bg-primary text-primary-foreground">
{username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</div>
{/* Message content */}
<div className="flex-1 min-w-0">
{/* Reply line and reference */}
{replyTo && replyToUser && (
<div className="flex items-center gap-2 mb-2 text-xs text-concord-secondary">
<div className="w-6 h-3 border-l-2 border-t-2 border-concord-secondary/50 rounded-tl-md ml-2" />
<span className="font-medium text-concord-primary">
{replyToUser.nickname || replyToUser.username}
</span>
<span className="truncate max-w-xs opacity-75">
{replyTo.text.replace(/```[\s\S]*?```/g, "[code]")}
</span>
</div>
)}
{/* Header - always show */}
<div className="flex items-baseline gap-2 mb-1">
<span className="font-semibold text-concord-primary">
{displayName}
</span>
<span className="text-xs text-concord-secondary">
{formatTimestamp(message.createdAt)}
</span>
{message.edited && (
<span className="text-xs text-concord-secondary opacity-60">
(edited)
</span>
)}
{(message as any).pinned && (
<Pin className="h-3 w-3 text-concord-primary" />
)}
</div>
{/* Message content with markdown */}
<div className="text-concord-primary leading-relaxed prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown
components={{
code: ({ className, children }) => {
const match = /language-(\w+)/.exec(className || "");
return match ? (
<div className="flex flex-row flex-1 max-w-2/3 flex-wrap !bg-transparent">
<SyntaxHighlighter
PreTag="div"
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={mode === "light" ? solarizedLight : dark}
className="!bg-concord-secondary p-2 border-2 concord-border rounded-xl"
/>
</div>
) : (
<code className={className}>{children}</code>
);
},
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-primary pl-4 my-2 italic text-concord-secondary bg-concord-secondary/30 py-2 rounded-r">
{children}
</blockquote>
),
p: ({ children }) => (
<p className="my-1 text-concord-primary">{children}</p>
),
strong: ({ children }) => (
<strong className="font-semibold text-concord-primary">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-concord-primary">{children}</em>
),
ul: ({ children }) => (
<ul className="list-disc list-inside my-2 text-concord-primary">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside my-2 text-concord-primary">
{children}
</ol>
),
h1: ({ children }) => (
<h1 className="text-xl font-bold my-2 text-concord-primary">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-lg font-bold my-2 text-concord-primary">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-base font-bold my-2 text-concord-primary">
{children}
</h3>
),
a: ({ children, href }) => (
<a
href={href}
className="text-primary hover:underline"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
}}
>
{message.text}
</ReactMarkdown>
</div>
</div>
{/* Message actions */}
{isHovered && (
<div className="absolute top-0 right-4 bg-concord-secondary border border-border rounded-md shadow-md flex">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 interactive-hover"
onClick={() => onReply?.(message.id)}
>
<Reply className="h-4 w-4" />
</Button>
<Button
variant="ghost"
className="h-8 w-8 p-0 interactive-hover"
onClick={() => navigator.clipboard.writeText(message.text)}
>
<Copy className="h-4 w-4" />
</Button>
{/*<MessageActionsDropdown
message={message}
onReply={() => onReply?.(message.id)}
/>*/}
</div>
)}
</div>
</div>
</>
);
};

View File

@@ -1,108 +0,0 @@
import { Message } from "@/lib/api-client";
import { useState, useRef, useEffect } from "react";
import { useSendMessage } from "@/hooks/useMessages";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
interface MessageUser {
id: string;
username?: string;
nickname?: string | null;
picture?: string | null;
}
interface MessageInputProps {
channelId: string;
channelName?: string;
replyingTo?: Message | null;
onCancelReply?: () => void;
replyingToUser: MessageUser | null;
}
export const MessageInput: React.FC<MessageInputProps> = ({
channelId,
channelName,
replyingTo,
onCancelReply,
replyingToUser,
}) => {
const [content, setContent] = useState("");
const formRef = useRef<HTMLFormElement>(null);
// Use the API hook for sending messages
const sendMessageMutation = useSendMessage();
// Auto-resize textarea (using direct DOM access as a fallback, no ref needed)
useEffect(() => {
const textarea = document.getElementById(
"message-input-textarea",
) as HTMLTextAreaElement | null;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [content]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (content.trim() && !sendMessageMutation.isPending) {
try {
await sendMessageMutation.mutateAsync({
channelId,
content: content.trim(),
repliedMessageId: replyingTo?.id || null,
});
setContent("");
onCancelReply?.();
} catch (error) {
console.error("Failed to send message:", error);
}
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
formRef.current?.requestSubmit();
}
};
return (
<div className="px-2 pb-2">
{replyingTo && replyingToUser && (
<div className="mb-2 p-3 bg-concord-secondary rounded-lg border border-b-0 border-border">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div className="w-6 h-4 border-l-2 border-t-2 border-concord-secondary/50 rounded-tl-md ml-2" />
<span className="font-medium text-concord-primary">
{replyingToUser.nickname || replyingToUser.username}
</span>
</div>
<Button
variant="ghost"
size="sm"
className="h-auto p-1 text-concord-secondary hover:text-concord-primary"
onClick={onCancelReply}
>
×
</Button>
</div>
<div className="text-sm text-concord-primary truncate pl-2">
{replyingTo.text.replace(/```[\s\S]*?```/g, "[code]")}
</div>
</div>
)}
<form ref={formRef} onSubmit={handleSubmit}>
<Textarea
id="message-input-textarea" // Unique ID for DOM targeting
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Message #${channelName || "channel"}`}
disabled={sendMessageMutation.isPending}
className="w-full bg-concord-tertiary border border-border rounded-lg px-4 py-3 text-concord-primary placeholder-concord-muted resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 min-h-8 max-h-56"
/>
</form>
</div>
);
};

View File

@@ -1,199 +0,0 @@
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Hash, Volume2, Loader2 } from "lucide-react";
import { useCreateChannel } from "@/hooks/useServers";
import { CategoryWithChannels } from "@/types";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "sonner";
interface CreateChannelModalProps {
isOpen: boolean;
onClose: () => void;
instanceId: string;
categories: CategoryWithChannels[] | undefined;
}
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
isOpen,
onClose,
categories,
}) => {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [type, setType] = useState<"text" | "voice">("text");
const [categoryId, setCategoryId] = useState("");
const createChannelMutation = useCreateChannel();
// Reset form when modal opens or closes
useEffect(() => {
if (!isOpen) {
setName("");
setDescription("");
setType("text");
setCategoryId("");
} else {
setCategoryId("");
}
}, [isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Basic validation: ensure name is not empty and a category is selected
if (!name.trim() || !categoryId || categoryId === "no-categories") {
console.warn("Channel name and a valid category are required.");
toast("Error", {
description: "Channel name and a valid category are required.",
});
return;
}
try {
await createChannelMutation.mutateAsync({
name: name.trim(),
description: description.trim(),
type,
categoryId,
});
// Reset form after successful creation
setName("");
setDescription("");
setType("text");
setCategoryId(""); // Reset to default or empty
onClose();
} catch (error) {
console.error("Failed to create channel:", error);
toast("Error", { description: <p>{`${error}`}</p> });
}
};
// Helper to determine if the form is in a valid state for submission
const isFormInvalid =
!name.trim() || // Name is required and cannot be just whitespace
!categoryId || // Category must be selected
categoryId === "no-categories" || // Handle the "no categories available" placeholder
createChannelMutation.isPending; // Disable while mutation is in progress
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Create Channel</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Channel Type Selection */}
<div className="space-y-2">
<Label>Channel Type</Label>
<div className="flex gap-2">
<Button
type="button"
variant={type === "text" ? "secondary" : "ghost"}
onClick={() => setType("text")}
className="flex-1"
>
<Hash className="h-4 w-4 mr-2" />
Text
</Button>
<Button
type="button"
variant={type === "voice" ? "secondary" : "ghost"}
onClick={() => setType("voice")}
className="flex-1"
>
<Volume2 className="h-4 w-4 mr-2" />
Voice
</Button>
</div>
</div>
{/* Channel Name Input */}
<div className="space-y-2">
<Label htmlFor="channel-name">Channel Name</Label>
<Input
id="channel-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="awesome-channel"
required
/>
</div>
{/* Category Selection */}
<div className="space-y-2">
<Label htmlFor="channel-category">Category</Label>
<Select
value={categoryId}
onValueChange={(value) => setCategoryId(value)}
required
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories && categories.length > 0 ? (
categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))
) : (
// Display this option if there are no categories
<SelectItem value="no-categories" disabled>
No categories available
</SelectItem>
)}
</SelectContent>
</Select>
</div>
{/* Channel Description Textarea */}
<div className="space-y-2">
<Label htmlFor="channel-description">Description</Label>
<Textarea
id="channel-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="What's this channel about?"
rows={3}
/>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={isFormInvalid}>
{createChannelMutation.isPending ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</div>
) : (
"Create Channel"
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,92 +0,0 @@
import React, { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useCreateInstance } from "@/hooks/useServers";
interface CreateServerModalProps {
isOpen: boolean;
onClose: () => void;
}
export const CreateServerModal: React.FC<CreateServerModalProps> = ({
isOpen,
onClose,
}) => {
const [name, setName] = useState("");
const [icon, setIcon] = useState("");
const createInstanceMutation = useCreateInstance();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
try {
await createInstanceMutation.mutateAsync({
name: name.trim(),
icon: icon.trim() || undefined,
});
// Reset form
setName("");
setIcon("");
onClose();
} catch (error) {
console.error("Failed to create server:", error);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Create Server</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="server-name">Server Name</Label>
<Input
id="server-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Awesome Server"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-icon">Server Icon URL (optional)</Label>
<Input
id="server-icon"
value={icon}
onChange={(e) => setIcon(e.target.value)}
placeholder="https://example.com/icon.png"
type="url"
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
disabled={!name.trim() || createInstanceMutation.isPending}
>
{createInstanceMutation.isPending
? "Creating..."
: "Create Server"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,59 +0,0 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Instance } from "@/types/database";
interface ServerIconProps {
server: Instance;
isActive: boolean;
onClick: () => void;
}
const ServerIcon: React.FC<ServerIconProps> = ({
server,
isActive,
onClick,
}) => {
const getServerInitials = (name: string) => {
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
return (
<div className="relative group w-12">
{/* Active indicator - Positioned outside to the left */}
<div
className={`absolute -left-2 top-1/2 transform -translate-y-1/2 w-1 bg-accent-foreground rounded transition-all duration-200 ${
isActive ? "h-10 rounded-xl" : "rounded-r h-2 group-hover:h-5"
}`}
/>
<Button
variant="ghost"
size="icon"
className={`w-12 h-12 transition-all duration-200 ${
isActive
? "rounded-xl border-primary bg-primary/10 border-2"
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
}`}
onClick={onClick}
>
{server.icon ? (
<img
src={server.icon}
alt={server.name}
className={`w-full h-full object-cover ${isActive ? "rounded-xl" : "rounded-2xl"}`}
/>
) : (
<span className="font-semibold text-sm">
{getServerInitials(server.name)}
</span>
)}
</Button>
</div>
);
};
export default ServerIcon;

View File

@@ -1,500 +0,0 @@
import { createContext, useContext, useEffect, useState } from "react";
export interface ThemeColors {
background: string;
foreground: string;
card: string;
cardForeground: string;
popover: string;
popoverForeground: string;
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
border: string;
input: string;
ring: string;
// Chart colors
chart1: string;
chart2: string;
chart3: string;
chart4: string;
chart5: string;
// Sidebar colors
sidebar: string;
sidebarForeground: string;
sidebarPrimary: string;
sidebarPrimaryForeground: string;
sidebarAccent: string;
sidebarAccentForeground: string;
sidebarBorder: string;
sidebarRing: string;
}
export interface ThemeDefinition {
id: string;
name: string;
description?: string;
mode: "light" | "dark";
colors: ThemeColors;
isCustom?: boolean;
}
// Fixed themes using proper OKLCH format
const DEFAULT_THEMES: ThemeDefinition[] = [
{
id: "default-light",
name: "Default Light",
mode: "light",
colors: {
background: "oklch(1 0 0)",
foreground: "oklch(0.145 0 0)",
card: "oklch(1 0 0)",
cardForeground: "oklch(0.145 0 0)",
popover: "oklch(1 0 0)",
popoverForeground: "oklch(0.145 0 0)",
primary: "oklch(0.205 0 0)",
primaryForeground: "oklch(0.985 0 0)",
secondary: "oklch(0.97 0 0)",
secondaryForeground: "oklch(0.205 0 0)",
muted: "oklch(0.97 0 0)",
mutedForeground: "oklch(0.556 0 0)",
accent: "oklch(0.97 0 0)",
accentForeground: "oklch(0.205 0 0)",
destructive: "oklch(0.577 0.245 27.325)",
border: "oklch(0.922 0 0)",
input: "oklch(0.922 0 0)",
ring: "oklch(0.708 0 0)",
chart1: "oklch(0.646 0.222 41.116)",
chart2: "oklch(0.6 0.118 184.704)",
chart3: "oklch(0.398 0.07 227.392)",
chart4: "oklch(0.828 0.189 84.429)",
chart5: "oklch(0.769 0.188 70.08)",
sidebar: "oklch(0.985 0 0)",
sidebarForeground: "oklch(0.145 0 0)",
sidebarPrimary: "oklch(0.205 0 0)",
sidebarPrimaryForeground: "oklch(0.985 0 0)",
sidebarAccent: "oklch(0.97 0 0)",
sidebarAccentForeground: "oklch(0.205 0 0)",
sidebarBorder: "oklch(0.922 0 0)",
sidebarRing: "oklch(0.708 0 0)",
},
},
{
id: "default-dark",
name: "Default Dark",
mode: "dark",
colors: {
background: "oklch(0.145 0 0)",
foreground: "oklch(0.985 0 0)",
card: "oklch(0.205 0 0)",
cardForeground: "oklch(0.985 0 0)",
popover: "oklch(0.205 0 0)",
popoverForeground: "oklch(0.985 0 0)",
primary: "oklch(0.922 0 0)",
primaryForeground: "oklch(0.205 0 0)",
secondary: "oklch(0.269 0 0)",
secondaryForeground: "oklch(0.985 0 0)",
muted: "oklch(0.269 0 0)",
mutedForeground: "oklch(0.708 0 0)",
accent: "oklch(0.269 0 0)",
accentForeground: "oklch(0.985 0 0)",
destructive: "oklch(0.704 0.191 22.216)",
border: "oklch(1 0 0 / 10%)",
input: "oklch(1 0 0 / 15%)",
ring: "oklch(0.556 0 0)",
chart1: "oklch(0.488 0.243 264.376)",
chart2: "oklch(0.696 0.17 162.48)",
chart3: "oklch(0.769 0.188 70.08)",
chart4: "oklch(0.627 0.265 303.9)",
chart5: "oklch(0.645 0.246 16.439)",
sidebar: "oklch(0.205 0 0)",
sidebarForeground: "oklch(0.985 0 0)",
sidebarPrimary: "oklch(0.488 0.243 264.376)",
sidebarPrimaryForeground: "oklch(0.985 0 0)",
sidebarAccent: "oklch(0.269 0 0)",
sidebarAccentForeground: "oklch(0.985 0 0)",
sidebarBorder: "oklch(1 0 0 / 10%)",
sidebarRing: "oklch(0.556 0 0)",
},
},
{
id: "paper-light",
name: "Paper",
description: "Clean paper-like theme",
mode: "light",
colors: {
background: "oklch(0.99 0.01 85)",
foreground: "oklch(0.15 0.02 65)",
card: "oklch(0.99 0.01 85)",
cardForeground: "oklch(0.15 0.02 65)",
popover: "oklch(1 0 0)",
popoverForeground: "oklch(0.15 0.02 65)",
primary: "oklch(0.25 0.03 45)",
primaryForeground: "oklch(0.98 0.01 85)",
secondary: "oklch(0.96 0.01 75)",
secondaryForeground: "oklch(0.25 0.03 45)",
muted: "oklch(0.96 0.01 75)",
mutedForeground: "oklch(0.45 0.02 55)",
accent: "oklch(0.96 0.01 75)",
accentForeground: "oklch(0.25 0.03 45)",
destructive: "oklch(0.577 0.245 27.325)",
border: "oklch(0.90 0.01 65)",
input: "oklch(0.90 0.01 65)",
ring: "oklch(0.25 0.03 45)",
chart1: "oklch(0.646 0.222 41.116)",
chart2: "oklch(0.6 0.118 184.704)",
chart3: "oklch(0.398 0.07 227.392)",
chart4: "oklch(0.828 0.189 84.429)",
chart5: "oklch(0.769 0.188 70.08)",
sidebar: "oklch(0.97 0.01 80)",
sidebarForeground: "oklch(0.15 0.02 65)",
sidebarPrimary: "oklch(0.25 0.03 45)",
sidebarPrimaryForeground: "oklch(0.98 0.01 85)",
sidebarAccent: "oklch(0.94 0.01 75)",
sidebarAccentForeground: "oklch(0.25 0.03 45)",
sidebarBorder: "oklch(0.88 0.01 65)",
sidebarRing: "oklch(0.25 0.03 45)",
},
},
{
id: "comfy-brown-dark",
name: "Comfy Brown",
description: "Warm brown theme for dark mode",
mode: "dark",
colors: {
background: "oklch(0.15 0.03 65)",
foreground: "oklch(0.95 0.01 85)",
card: "oklch(0.20 0.03 55)",
cardForeground: "oklch(0.95 0.01 85)",
popover: "oklch(0.20 0.03 55)",
popoverForeground: "oklch(0.95 0.01 85)",
primary: "oklch(0.65 0.15 45)",
primaryForeground: "oklch(0.95 0.01 85)",
secondary: "oklch(0.25 0.04 50)",
secondaryForeground: "oklch(0.95 0.01 85)",
muted: "oklch(0.25 0.04 50)",
mutedForeground: "oklch(0.70 0.02 65)",
accent: "oklch(0.25 0.04 50)",
accentForeground: "oklch(0.95 0.01 85)",
destructive: "oklch(0.704 0.191 22.216)",
border: "oklch(0.30 0.04 55)",
input: "oklch(0.30 0.04 55)",
ring: "oklch(0.65 0.15 45)",
chart1: "oklch(0.65 0.15 45)",
chart2: "oklch(0.55 0.12 85)",
chart3: "oklch(0.75 0.18 25)",
chart4: "oklch(0.60 0.14 105)",
chart5: "oklch(0.70 0.16 65)",
sidebar: "oklch(0.18 0.03 60)",
sidebarForeground: "oklch(0.95 0.01 85)",
sidebarPrimary: "oklch(0.65 0.15 45)",
sidebarPrimaryForeground: "oklch(0.95 0.01 85)",
sidebarAccent: "oklch(0.22 0.04 50)",
sidebarAccentForeground: "oklch(0.95 0.01 85)",
sidebarBorder: "oklch(0.28 0.04 55)",
sidebarRing: "oklch(0.65 0.15 45)",
},
},
{
id: "midnight-dark",
name: "Midnight",
description: "Deep blue midnight theme",
mode: "dark",
colors: {
background: "oklch(0.12 0.08 250)",
foreground: "oklch(0.95 0.01 230)",
card: "oklch(0.18 0.06 240)",
cardForeground: "oklch(0.95 0.01 230)",
popover: "oklch(0.18 0.06 240)",
popoverForeground: "oklch(0.95 0.01 230)",
primary: "oklch(0.60 0.20 240)",
primaryForeground: "oklch(0.95 0.01 230)",
secondary: "oklch(0.22 0.05 235)",
secondaryForeground: "oklch(0.95 0.01 230)",
muted: "oklch(0.22 0.05 235)",
mutedForeground: "oklch(0.70 0.02 230)",
accent: "oklch(0.22 0.05 235)",
accentForeground: "oklch(0.95 0.01 230)",
destructive: "oklch(0.704 0.191 22.216)",
border: "oklch(0.25 0.05 235)",
input: "oklch(0.25 0.05 235)",
ring: "oklch(0.60 0.20 240)",
chart1: "oklch(0.60 0.20 240)",
chart2: "oklch(0.50 0.15 200)",
chart3: "oklch(0.65 0.18 280)",
chart4: "oklch(0.55 0.16 160)",
chart5: "oklch(0.70 0.22 300)",
sidebar: "oklch(0.15 0.07 245)",
sidebarForeground: "oklch(0.95 0.01 230)",
sidebarPrimary: "oklch(0.60 0.20 240)",
sidebarPrimaryForeground: "oklch(0.95 0.01 230)",
sidebarAccent: "oklch(0.20 0.05 235)",
sidebarAccentForeground: "oklch(0.95 0.01 230)",
sidebarBorder: "oklch(0.22 0.05 235)",
sidebarRing: "oklch(0.60 0.20 240)",
},
},
];
type ThemeMode = "light" | "dark" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: ThemeMode;
storageKey?: string;
};
type ThemeProviderState = {
mode: ThemeMode;
currentTheme: ThemeDefinition;
currentLightTheme: ThemeDefinition;
currentDarkTheme: ThemeDefinition;
themes: ThemeDefinition[];
setMode: (mode: ThemeMode) => void;
setTheme: (themeId: string) => void;
addCustomTheme: (theme: Omit<ThemeDefinition, "id" | "isCustom">) => void;
removeCustomTheme: (themeId: string) => void;
getThemesForMode: (mode: "light" | "dark") => ThemeDefinition[];
};
const initialState: ThemeProviderState = {
mode: "system",
currentTheme: DEFAULT_THEMES[1], // Default to dark theme
currentLightTheme: DEFAULT_THEMES[0], // Default light
currentDarkTheme: DEFAULT_THEMES[1], // Default dark
themes: DEFAULT_THEMES,
setMode: () => null,
setTheme: () => null,
addCustomTheme: () => null,
removeCustomTheme: () => null,
getThemesForMode: () => [],
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "concord-theme",
...props
}: ThemeProviderProps) {
const [mode, setMode] = useState<ThemeMode>(
() =>
(localStorage.getItem(storageKey + "-mode") as ThemeMode) || defaultTheme,
);
const [themes, setThemes] = useState<ThemeDefinition[]>(() => {
const saved = localStorage.getItem(storageKey + "-themes");
const customThemes = saved ? JSON.parse(saved) : [];
return [...DEFAULT_THEMES, ...customThemes];
});
const [currentLightThemeId, setCurrentLightThemeId] = useState<string>(() => {
const saved = localStorage.getItem(storageKey + "-light");
return saved || "default-light";
});
const [currentDarkThemeId, setCurrentDarkThemeId] = useState<string>(() => {
const saved = localStorage.getItem(storageKey + "-dark");
return saved || "default-dark";
});
const currentLightTheme =
themes.find((t) => t.id === currentLightThemeId) || DEFAULT_THEMES[0];
const currentDarkTheme =
themes.find((t) => t.id === currentDarkThemeId) || DEFAULT_THEMES[2];
// Determine the current theme based on mode and system preference
const getCurrentTheme = (): ThemeDefinition => {
switch (mode) {
case "light":
return currentLightTheme;
case "dark":
return currentDarkTheme;
case "system":
const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
return systemPrefersDark ? currentDarkTheme : currentLightTheme;
default:
return currentDarkTheme;
}
};
const currentTheme = getCurrentTheme();
const applyTheme = (theme: ThemeDefinition) => {
const root = window.document.documentElement;
// Remove existing theme classes
root.classList.remove("light", "dark");
// Apply mode class
root.classList.add(theme.mode);
// Apply CSS custom properties with proper mapping
Object.entries(theme.colors).forEach(([key, value]) => {
// Convert camelCase to kebab-case and map to CSS variables
const cssVarMap: Record<string, string> = {
background: "--background",
foreground: "--foreground",
card: "--card",
cardForeground: "--card-foreground",
popover: "--popover",
popoverForeground: "--popover-foreground",
primary: "--primary",
primaryForeground: "--primary-foreground",
secondary: "--secondary",
secondaryForeground: "--secondary-foreground",
muted: "--muted",
mutedForeground: "--muted-foreground",
accent: "--accent",
accentForeground: "--accent-foreground",
destructive: "--destructive",
border: "--border",
input: "--input",
ring: "--ring",
chart1: "--chart-1",
chart2: "--chart-2",
chart3: "--chart-3",
chart4: "--chart-4",
chart5: "--chart-5",
sidebar: "--sidebar",
sidebarForeground: "--sidebar-foreground",
sidebarPrimary: "--sidebar-primary",
sidebarPrimaryForeground: "--sidebar-primary-foreground",
sidebarAccent: "--sidebar-accent",
sidebarAccentForeground: "--sidebar-accent-foreground",
sidebarBorder: "--sidebar-border",
sidebarRing: "--sidebar-ring",
};
const cssVar = cssVarMap[key];
if (cssVar) {
root.style.setProperty(cssVar, value);
}
});
};
useEffect(() => {
applyTheme(currentTheme);
}, [currentTheme]);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
if (mode === "system") {
// Theme will be recalculated due to getCurrentTheme dependency
const newTheme = getCurrentTheme();
applyTheme(newTheme);
}
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [mode, currentLightTheme, currentDarkTheme]);
const setTheme = (themeId: string) => {
const theme = themes.find((t) => t.id === themeId);
if (!theme) return;
// Update the appropriate theme based on the theme's mode
if (theme.mode === "light") {
setCurrentLightThemeId(themeId);
localStorage.setItem(storageKey + "-light", themeId);
} else {
setCurrentDarkThemeId(themeId);
localStorage.setItem(storageKey + "-dark", themeId);
}
};
const handleSetMode = (newMode: ThemeMode) => {
setMode(newMode);
localStorage.setItem(storageKey + "-mode", newMode);
};
const addCustomTheme = (
themeData: Omit<ThemeDefinition, "id" | "isCustom">,
) => {
const newTheme: ThemeDefinition = {
...themeData,
id: `custom-${Date.now()}`,
isCustom: true,
};
const updatedThemes = [...themes, newTheme];
setThemes(updatedThemes);
// Save only custom themes to localStorage
const customThemes = updatedThemes.filter((t) => t.isCustom);
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
};
const removeCustomTheme = (themeId: string) => {
const updatedThemes = themes.filter((t) => t.id !== themeId);
setThemes(updatedThemes);
// If removing current theme, switch to default
if (currentLightThemeId === themeId) {
const defaultLight = updatedThemes.find(
(t) => t.mode === "light" && !t.isCustom,
);
if (defaultLight) {
setCurrentLightThemeId(defaultLight.id);
localStorage.setItem(storageKey + "-light", defaultLight.id);
}
}
if (currentDarkThemeId === themeId) {
const defaultDark = updatedThemes.find(
(t) => t.mode === "dark" && !t.isCustom,
);
if (defaultDark) {
setCurrentDarkThemeId(defaultDark.id);
localStorage.setItem(storageKey + "-dark", defaultDark.id);
}
}
// Save only custom themes to localStorage
const customThemes = updatedThemes.filter((t) => t.isCustom);
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
};
const getThemesForMode = (targetMode: "light" | "dark") => {
return themes.filter((t) => t.mode === targetMode);
};
const value = {
mode,
currentTheme,
currentLightTheme,
currentDarkTheme,
themes,
setMode: handleSetMode,
setTheme,
addCustomTheme,
removeCustomTheme,
getThemesForMode,
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -1,66 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@@ -1,51 +0,0 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,46 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -1,58 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -1,92 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -1,141 +0,0 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -1,255 +0,0 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -1,21 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -1,22 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -1,56 +0,0 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -1,183 +0,0 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@@ -1,61 +0,0 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }

View File

@@ -1,23 +0,0 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@@ -1,29 +0,0 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -1,18 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,59 +0,0 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -1,39 +0,0 @@
import React, { useEffect, useRef } from "react";
import { useVoiceStore } from "@/stores/voiceStore";
interface AudioPlayerProps {
stream: MediaStream;
isDeafened: boolean;
}
const AudioPlayer: React.FC<AudioPlayerProps> = ({ stream, isDeafened }) => {
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
if (audioRef.current) {
audioRef.current.srcObject = stream;
audioRef.current.volume = isDeafened ? 0 : 1;
}
}, [stream, isDeafened]);
return <audio ref={audioRef} autoPlay playsInline />;
};
const VoiceConnectionManager: React.FC = () => {
const remoteStreams = useVoiceStore((state) => state.remoteStreams);
const isDeafened = useVoiceStore((state) => state.isDeafened);
if (remoteStreams.size === 0) {
return null;
}
return (
<div style={{ display: "none" }}>
{Array.from(remoteStreams.entries()).map(([userId, stream]) => (
<AudioPlayer key={userId} stream={stream} isDeafened={isDeafened} />
))}
</div>
);
};
export default VoiceConnectionManager;

View File

@@ -1,212 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthStore } from "@/stores/authStore";
import {
authClient,
LoginCredentials,
RegisterData,
AuthResponse,
} from "@/lib/auth-client";
import { BackendUser } from "@/lib/api-client";
import { Role, UserStatus } from "@/types/database";
// Frontend User type
interface FrontendUser {
id: string;
username: string;
nickname?: string | null;
bio?: string | null;
picture?: string | null;
banner?: string | null;
hashPassword: string;
admin: boolean;
status: UserStatus;
createdAt: string;
updatedAt: string;
roles: Role[];
}
// Transform backend user to frontend user format
function transformBackendUser(backendUser: BackendUser): FrontendUser {
return {
id: backendUser.id,
username: backendUser.userName,
nickname: backendUser.nickName,
bio: backendUser.bio,
picture: backendUser.picture,
banner: backendUser.banner,
hashPassword: "", // Don't store password
admin: backendUser.admin,
status: transformStatusToFrontend(backendUser.status),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
roles: backendUser.role.map((r) => ({
instanceId: r.instanceId || "",
role: r.role || "member",
})) as Role[],
};
}
// Transform status from backend to frontend format
function transformStatusToFrontend(
backendStatus: "online" | "offline" | "dnd" | "idle" | "invis",
): UserStatus {
switch (backendStatus) {
case "dnd":
return "busy";
case "idle":
return "away";
case "invis":
return "offline";
default:
return "online";
}
}
// Transform status from frontend to backend format
export function transformStatusToBackend(
frontendStatus: UserStatus,
): "online" | "offline" | "dnd" | "idle" | "invis" {
switch (frontendStatus) {
case "busy":
return "dnd";
case "away":
return "idle";
case "offline":
return "invis";
default:
return "online";
}
}
// Hook for login
export const useLogin = () => {
const { setAuth, setLoading } = useAuthStore();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
credentials: LoginCredentials,
): Promise<AuthResponse> => {
setLoading(true);
return authClient.login(credentials);
},
onSuccess: (data: AuthResponse) => {
const frontendUser = transformBackendUser(data.user);
setAuth(frontendUser, data.token, data.token); // Use token as refresh token for now
queryClient.clear();
},
onError: (error: Error) => {
console.error("Login failed:", error);
setLoading(false);
},
onSettled: () => {
setLoading(false);
},
});
};
// Hook for registration (requires admin)
export const useRegister = () => {
const { setAuth, setLoading, user, token } = useAuthStore();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userData: RegisterData): Promise<AuthResponse> => {
setLoading(true);
if (!user || !token || !user.admin) {
throw new Error("Admin privileges required for user creation");
}
return authClient.register(userData, { id: user.id, token });
},
onSuccess: (data: AuthResponse) => {
const frontendUser = transformBackendUser(data.user);
setAuth(frontendUser, data.token, data.token);
queryClient.clear();
},
onError: (error: Error) => {
console.error("Registration failed:", error);
setLoading(false);
},
onSettled: () => {
setLoading(false);
},
});
};
// Hook for logout
export const useLogout = () => {
const { logout, user } = useAuthStore();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (): Promise<void> => {
if (user) {
try {
await authClient.logout(user.id);
} catch (error) {
console.warn("Logout endpoint failed:", error);
}
}
},
onSuccess: () => {
logout();
queryClient.clear();
},
onError: (error: Error) => {
console.error("Logout failed:", error);
// Still logout locally even if server request fails
logout();
queryClient.clear();
},
});
};
// Hook for token validation
export const useValidateToken = () => {
const { token, user, setAuth, logout } = useAuthStore();
return useMutation({
mutationFn: async (): Promise<{ valid: boolean; user?: BackendUser }> => {
if (!token || !user) {
throw new Error("No token to validate");
}
return authClient.validateToken(token, user.id);
},
onSuccess: (data) => {
if (!data.valid) {
logout();
} else if (data.user) {
const frontendUser = transformBackendUser(data.user);
setAuth(frontendUser, token!, useAuthStore.getState().refreshToken!);
}
},
onError: (error: Error) => {
console.error("Token validation failed:", error);
logout();
},
});
};
// Hook for token refresh
export const useRefreshToken = () => {
const { refreshToken, user, setAuth, logout } = useAuthStore();
return useMutation({
mutationFn: async (): Promise<AuthResponse> => {
if (!refreshToken || !user) {
throw new Error("No refresh token available");
}
return authClient.refreshToken(refreshToken, user.id);
},
onSuccess: (data: AuthResponse) => {
const frontendUser = transformBackendUser(data.user);
setAuth(frontendUser, data.token, data.token);
},
onError: (error: Error) => {
console.error("Token refresh failed:", error);
logout();
},
});
};

View File

@@ -1,303 +0,0 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient, Message } from "@/lib/api-client";
import { useAuthStore } from "@/stores/authStore";
// Hook for getting messages in a channel with pagination
export const useChannelMessages = (channelId?: string, limit = 50) => {
return useQuery({
queryKey: ["messages", channelId, limit],
queryFn: async (): Promise<Message[]> => {
if (!channelId) return [];
try {
const date = new Date();
const messages = await apiClient.getMessages({
date: date.toISOString(),
channelId: channelId,
});
return messages || [];
} catch (error) {
console.error("Failed to fetch messages:", error);
return [];
}
},
enabled: !!channelId,
staleTime: 500 * 1,
refetchInterval: 500 * 1,
});
};
// Hook for getting older messages (pagination)
export const useChannelMessagesPaginated = (
channelId?: string,
beforeDate?: Date,
limit = 50,
) => {
return useQuery({
queryKey: [
"messages",
channelId,
"paginated",
beforeDate?.toISOString(),
limit,
],
queryFn: async (): Promise<Message[]> => {
if (!channelId || !beforeDate) return [];
try {
const messages = await apiClient.getMessages({
date: beforeDate.toISOString(),
channelId: channelId,
});
return messages || [];
} catch (error) {
console.error("Failed to fetch paginated messages:", error);
return [];
}
},
enabled: !!channelId && !!beforeDate,
staleTime: 500 * 1,
});
};
// Hook for sending messages
export const useSendMessage = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: {
channelId: string;
content: string;
repliedMessageId?: string | null;
}) => {
if (!user || !token) {
throw new Error("Authentication required");
}
const requestData = {
channelId: data.channelId,
userId: user.id,
content: data.content,
token: token,
repliedMessageId: data.repliedMessageId,
};
try {
const message = await apiClient.sendMessage(requestData);
return message;
} catch (error) {
console.error("Failed to send message:", error);
throw new Error("Failed to send message");
}
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["messages", variables.channelId],
});
},
onError: (error) => {
console.error("Send message failed:", error);
},
});
};
// Hook for deleting messages
export const useDeleteMessage = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: { messageId: string; channelId: string }) => {
if (!user || !token) {
throw new Error("Authentication required");
}
// TODO: Replace with actual API call when available
return { success: true, messageId: data.messageId };
},
onSuccess: (result, variables) => {
// Update the cache to mark message as deleted
queryClient.setQueryData(
["messages", variables.channelId],
(oldData: Message[] | undefined) => {
if (!oldData) return oldData;
return oldData.map((msg) =>
msg.id === result.messageId
? { ...msg, content: "[Message deleted]", deleted: true }
: msg,
);
},
);
},
onError: (error) => {
console.error("Delete message failed:", error);
},
});
};
// Hook for editing messages
export const useEditMessage = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: {
messageId: string;
content: string;
channelId: string;
}) => {
if (!user || !token) {
throw new Error("Authentication required");
}
// TODO: Replace with actual API call when available
return {
success: true,
messageId: data.messageId,
content: data.content,
edited: true,
};
},
onSuccess: (result, variables) => {
// Update the cache with edited message
queryClient.setQueryData(
["messages", variables.channelId],
(oldData: Message[] | undefined) => {
if (!oldData) return oldData;
return oldData.map((msg) =>
msg.id === result.messageId
? { ...msg, content: result.content, edited: result.edited }
: msg,
);
},
);
},
onError: (error) => {
console.error("Edit message failed:", error);
},
});
};
// Hook for pinning/unpinning messages
export const usePinMessage = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: {
messageId: string;
channelId: string;
pinned: boolean;
}) => {
if (!user || !token) {
throw new Error("Authentication required");
}
// TODO: Replace with actual API call when available
return {
success: true,
messageId: data.messageId,
pinned: data.pinned,
};
},
onSuccess: (result, variables) => {
// Update the cache with pinned status
queryClient.setQueryData(
["messages", variables.channelId],
(oldData: Message[] | undefined) => {
if (!oldData) return oldData;
return oldData.map((msg) =>
msg.id === result.messageId
? { ...msg, pinned: result.pinned }
: msg,
);
},
);
// Also invalidate pinned messages query if it exists
queryClient.invalidateQueries({
queryKey: ["pinned-messages", variables.channelId],
});
},
onError: (error) => {
console.error("Pin message failed:", error);
},
});
};
// Hook for getting pinned messages
export const usePinnedMessages = (channelId?: string) => {
return useQuery({
queryKey: ["pinned-messages", channelId],
queryFn: async (): Promise<Message[]> => {
if (!channelId) return [];
try {
// TODO: Replace with actual API call when available
// For now, return empty array
return [];
} catch (error) {
console.error("Failed to fetch pinned messages:", error);
return [];
}
},
enabled: !!channelId,
staleTime: 500 * 1,
});
};
// Hook for loading more messages (infinite scroll)
export const useLoadMoreMessages = (channelId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: { beforeDate: Date }) => {
if (!channelId) return [];
try {
const messages = await apiClient.getMessages({
date: data.beforeDate.toISOString(),
channelId: channelId,
});
return messages || [];
} catch (error) {
console.error("Failed to load more messages:", error);
return [];
}
},
onSuccess: (newMessages) => {
if (newMessages.length > 0) {
// Prepend new messages to existing messages
queryClient.setQueryData(
["messages", channelId],
(oldData: Message[] | undefined) => {
if (!oldData) return newMessages;
// Remove duplicates and sort by creation date
const combined = [...newMessages, ...oldData];
const unique = combined.filter(
(msg, index, arr) =>
arr.findIndex((m) => m.id === msg.id) === index,
);
return unique.sort(
(a, b) =>
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime(),
);
},
);
}
},
});
};

View File

@@ -1,108 +0,0 @@
import { useAuthStore } from "@/stores/authStore";
import { Role } from "@/types/database";
import { useMemo } from "react";
import { useParams } from "react-router";
type PermissionsRole = "admin" | "member" | "mod";
const getUserRoleForInstance = (
roles: Role[],
instanceId: string,
): PermissionsRole => {
if (!instanceId) return "member";
const roleEntry = roles.find((r) => r.instanceId === instanceId);
return roleEntry?.role || "member";
};
const getRoleInfo = (role: PermissionsRole) => {
const lowerRole = role.toLowerCase();
switch (lowerRole) {
case "admin":
return { color: "#ff6b6b", priority: 3, name: "Admin" };
case "mod":
return { color: "#4ecdc4", priority: 2, name: "Moderator" };
case "member":
return { color: null, priority: 1, name: "Member" };
default:
return {
color: null,
priority: 0,
name: role.charAt(0).toUpperCase() + role.slice(1),
};
}
};
interface InstancePermissions {
currentUserRole: PermissionsRole;
currentUserRolePriority: number;
canManageMembers: boolean; // Can kick/ban/promote/demote members
canViewAdminPanel: boolean;
}
export const useInstancePermissions = (): InstancePermissions => {
const { instanceId } = useParams<{ instanceId: string }>();
const { user: currentUser } = useAuthStore();
const permissions = useMemo(() => {
let currentUserRole: PermissionsRole = "member";
let currentUserRolePriority = 1;
let canManageMembers = false;
let canViewAdminPanel = false;
if (!currentUser || !instanceId) {
// If no user or instance, user has no permissions within an instance
return {
currentUserRole,
currentUserRolePriority,
canManageMembers,
canViewAdminPanel,
};
}
// If they are a global admin
if (currentUser.admin) {
currentUserRole = "admin";
currentUserRolePriority = 3;
canManageMembers = true;
canViewAdminPanel = true;
return {
currentUserRole: "admin",
currentUserRolePriority: 3,
canManageMembers: true,
canManageRoles: true,
canViewAdminPanel: true,
};
}
// Instance-Specific Role Check
const instanceRole = getUserRoleForInstance(currentUser.roles, instanceId);
const roleInfo = getRoleInfo(instanceRole as PermissionsRole);
currentUserRole = instanceRole;
currentUserRolePriority = roleInfo.priority;
// Define permissions based on role priority
if (roleInfo.priority >= 3) {
// Admin
canManageMembers = true;
canViewAdminPanel = true;
} else if (roleInfo.priority === 2) {
// Moderator
canManageMembers = true;
canViewAdminPanel = false;
} else {
// Member (priority 1 or 0)
canManageMembers = false;
canViewAdminPanel = false;
}
return {
currentUserRole,
currentUserRolePriority,
canManageMembers,
canViewAdminPanel,
};
}, [currentUser, instanceId]);
return permissions as InstancePermissions;
};

View File

@@ -1,269 +0,0 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
apiClient,
Instance,
Category,
Channel,
BackendUser,
} from "@/lib/api-client";
import { useAuthStore } from "@/stores/authStore";
// Extended types with relations for frontend use
export interface CategoryWithChannels extends Category {
channels: Channel[];
}
export interface InstanceWithDetails extends Instance {
categories: CategoryWithChannels[];
}
// Transform backend user to frontend user format for compatibility
function transformBackendUserToFrontend(backendUser: BackendUser) {
return {
id: backendUser.id,
username: backendUser.userName,
nickname: backendUser.nickName,
bio: backendUser.bio,
picture: backendUser.picture,
banner: backendUser.banner,
hashPassword: "",
admin: backendUser.admin,
status:
backendUser.status === "dnd"
? "busy"
: backendUser.status === "idle"
? "away"
: backendUser.status === "invis"
? "offline"
: backendUser.status,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
roles: backendUser.role.map((r) => ({
instanceId: r.instanceId || "",
role: r.role || "member",
})),
};
}
// Hook for getting all servers/instances
export const useServers = () => {
return useQuery({
queryKey: ["servers"],
queryFn: async (): Promise<Instance[]> => {
try {
const instances = await apiClient.getInstances();
return instances;
} catch (error) {
console.error("Failed to fetch servers:", error);
throw new Error("Failed to fetch servers");
}
},
staleTime: 500 * 1,
});
};
// Hook for getting detailed instance info with categories and channels
export const useInstanceDetails = (instanceId?: string) => {
return useQuery({
queryKey: ["instance", instanceId],
queryFn: async (): Promise<InstanceWithDetails | null> => {
if (!instanceId) return null;
try {
// Get instance basic info
const instances = await apiClient.getInstances();
const instance = instances.find((s) => s.id === instanceId);
if (!instance) return null;
// Get categories for this instance
const categories = await apiClient.getCategoriesByInstance(instanceId);
// For each category, get its channels
const categoriesWithChannels: CategoryWithChannels[] =
await Promise.all(
categories.map(async (category): Promise<CategoryWithChannels> => {
try {
const channels = await apiClient.getChannelsByCategory(
category.id,
);
return {
...category,
channels: channels || [],
};
} catch (error) {
console.warn(
`Failed to fetch channels for category ${category.id}:`,
error,
);
return {
...category,
channels: [],
};
}
}),
);
return {
...instance,
categories: categoriesWithChannels,
};
} catch (error) {
console.error("Failed to fetch instance details:", error);
throw new Error("Failed to fetch instance details");
}
},
enabled: !!instanceId,
staleTime: 500 * 1,
});
};
// Hook for getting all users from an instance with their roles
export const useInstanceMembers = (instanceId?: string) => {
return useQuery({
queryKey: ["instance", instanceId, "members"],
queryFn: async () => {
if (!instanceId) return [];
try {
const backendUsers = await apiClient.getUsersByInstance(instanceId);
// Transform backend users to frontend format for compatibility
return backendUsers.map(transformBackendUserToFrontend);
} catch (error) {
console.error("Failed to fetch instance members:", error);
throw new Error("Failed to fetch instance members");
}
},
enabled: !!instanceId,
staleTime: 500 * 1,
});
};
// Hook for creating a new server/instance
export const useCreateInstance = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: { name: string; icon?: string }) => {
if (!user || !token) {
throw new Error("Authentication required");
}
const requestData = {
...data,
requestingUserId: user.id,
requestingUserToken: token,
};
try {
const instance = await apiClient.createInstance(requestData);
return instance;
} catch (error) {
console.error("Failed to create instance:", error);
throw new Error("Failed to create instance");
}
},
onSuccess: () => {
// Invalidate servers list to refetch
queryClient.invalidateQueries({ queryKey: ["servers"] });
},
});
};
// Hook for creating a new category
export const useCreateCategory = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: {
name: string;
instanceId?: string;
position: number;
}) => {
if (!user || !token) {
throw new Error("Authentication required");
}
const requestData = {
...data,
admin: user.admin,
requestingUserId: user.id,
requestingUserToken: token,
};
try {
const category = await apiClient.createCategory(requestData);
return category;
} catch (error) {
console.error("Failed to create category:", error);
throw new Error("Failed to create category");
}
},
onSuccess: (_, variables) => {
// Invalidate instance details to refetch categories
if (variables.instanceId) {
queryClient.invalidateQueries({
queryKey: ["instance", variables.instanceId],
});
}
},
});
};
// Hook for creating a new channel
export const useCreateChannel = () => {
const queryClient = useQueryClient();
const { user, token } = useAuthStore();
return useMutation({
mutationFn: async (data: {
type: "text" | "voice";
name: string;
description: string;
categoryId?: string;
}) => {
if (!user || !token) {
throw new Error("Authentication required");
}
const requestData = {
...data,
admin: user.admin,
requestingUserId: user.id,
requestingUserToken: token,
};
try {
const channel = await apiClient.createChannel(requestData);
return channel;
} catch (error) {
console.error("Failed to create channel:", error);
throw new Error("Failed to create channel");
}
},
onSuccess: (_, variables) => {
// Invalidate related queries
if (variables.categoryId) {
// Find the instance this category belongs to and invalidate it
queryClient.invalidateQueries({
queryKey: ["instance"],
});
}
},
});
};
// Placeholder hook for channels by instance (for backward compatibility)
export const useChannels = (instanceId?: string) => {
const { data: instance } = useInstanceDetails(instanceId);
return useQuery({
queryKey: ["channels", instanceId],
queryFn: async (): Promise<CategoryWithChannels[]> => {
return instance?.categories || [];
},
enabled: !!instanceId && !!instance,
staleTime: 500 * 1,
});
};

View File

@@ -1,307 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* concord-specific custom properties */
--color-text-primary: var(--foreground);
--color-text-secondary: var(--muted-foreground);
--color-text-muted: var(--muted-foreground);
--color-interactive-normal: var(--muted-foreground);
--color-interactive-hover: var(--foreground);
--color-interactive-active: var(--primary);
--color-background-primary: var(--background);
--color-background-secondary: var(--card);
--color-background-tertiary: var(--muted);
/* Status colors - these remain consistent across themes */
--color-status-online: oklch(0.6 0.15 140);
--color-status-away: oklch(0.75 0.15 85);
--color-status-busy: oklch(0.65 0.2 25);
--color-status-offline: var(--muted-foreground);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
border-color: var(--border);
outline-color: var(--ring, oklch(0.708 0 0)) / 50%;
}
body {
background-color: var(--background);
color: var(--foreground);
}
}
/* Background utilities */
@utility bg-concord-primary {
background-color: var(--color-background-primary);
}
@utility bg-concord-secondary {
background-color: var(--color-background-secondary);
}
@utility bg-concord-tertiary {
background-color: var(--color-background-tertiary);
}
/* Text utilities */
@utility text-concord-primary {
color: var(--color-text-primary);
}
@utility text-concord-secondary {
color: var(--color-text-secondary);
}
@utility text-concord-muted {
color: var(--color-text-muted);
}
/* Interactive utilities */
@utility text-interactive-normal {
color: var(--color-interactive-normal);
}
@utility text-interactive-hover {
color: var(--color-interactive-hover);
}
@utility text-interactive-active {
color: var(--color-interactive-active);
}
/* Status utilities */
@utility text-status-online {
color: var(--color-status-online);
}
@utility text-status-away {
color: var(--color-status-away);
}
@utility text-status-busy {
color: var(--color-status-busy);
}
@utility text-status-offline {
color: var(--color-status-offline);
}
@utility bg-status-online {
background-color: var(--color-status-online);
}
@utility bg-status-away {
background-color: var(--color-status-away);
}
@utility bg-status-busy {
background-color: var(--color-status-busy);
}
@utility bg-status-offline {
background-color: var(--color-status-offline);
}
/* Border utilities */
@utility border-concord {
border-color: var(--color-border);
}
@utility border-sidebar {
border-color: var(--color-sidebar-border);
}
@utility interactive-hover {
transition-property: color;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
color: var(--color-interactive-normal);
&:hover {
color: var(--color-interactive-hover);
}
}
@utility spacing-xs {
padding: var(--tw-spacing-1, 0.25rem);
gap: var(--tw-spacing-1, 0.25rem);
}
@utility spacing-sm {
padding: var(--tw-spacing-2, 0.5rem);
gap: var(--tw-spacing-2, 0.5rem);
}
@utility spacing-md {
padding: var(--tw-spacing-4, 1rem);
gap: var(--tw-spacing-4, 1rem);
}
@utility spacing-lg {
padding: var(--tw-spacing-6, 1.5rem);
gap: var(--tw-spacing-6, 1.5rem);
}
@utility spacing-xl {
padding: var(--tw-spacing-8, 2rem);
gap: var(--tw-spacing-8, 2rem);
}
@utility spacing-x-xs {
padding-left: var(--tw-spacing-1, 0.25rem);
padding-right: var(--tw-spacing-1, 0.25rem);
}
@utility spacing-x-sm {
padding-left: var(--tw-spacing-2, 0.5rem);
padding-right: var(--tw-spacing-2, 0.5rem);
}
@utility spacing-x-md {
padding-left: var(--tw-spacing-4, 1rem);
padding-right: var(--tw-spacing-4, 1rem);
}
@utility spacing-x-lg {
padding-left: var(--tw-spacing-6, 1.5rem);
padding-right: var(--tw-spacing-6, 1.5rem);
}
@utility spacing-x-xl {
padding-left: var(--tw-spacing-8, 2rem);
padding-right: var(--tw-spacing-8, 2rem);
}
@utility spacing-y-xs {
padding-top: var(--tw-spacing-1, 0.25rem);
padding-bottom: var(--tw-spacing-1, 0.25rem);
}
@utility spacing-y-sm {
padding-top: var(--tw-spacing-2, 0.5rem);
padding-bottom: var(--tw-spacing-2, 0.5rem);
}
@utility spacing-y-md {
padding-top: var(--tw-spacing-4, 1rem);
padding-bottom: var(--tw-spacing-4, 1rem);
}
@utility spacing-y-lg {
padding-top: var(--tw-spacing-6, 1.5rem);
padding-bottom: var(--tw-spacing-6, 1.5rem);
}
@utility spacing-y-xl {
padding-top: var(--tw-spacing-8, 2rem);
padding-bottom: var(--tw-spacing-8, 2rem);
}
@layer components {
/* Sidebar styling */
.sidebar-primary {
background-color: var(--color-sidebar);
border-right-width: 1px;
border-color: var(--color-sidebar-border);
}
.sidebar-secondary {
background-color: var(--color-background-secondary);
border-right-width: 1px;
border-color: var(--color-border);
}
.panel-button {
display: flex;
width: full;
flex-grow: 1;
border-radius: 12px;
margin: 4px 8px;
padding: 8px;
&:hover {
background-color: var(--color-background-tertiary);
}
}
}

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