Restarted the project.
Old one is at the branch @v0.
This commit is contained in:
parent
e77bcdf4e7
commit
2afc18ee99
151 changed files with 7922 additions and 12578 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "next"
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -28,6 +28,7 @@
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
|
|
||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
|
|
@ -1,8 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
14
.idea/discord.xml
generated
14
.idea/discord.xml
generated
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
<option name="applicationTheme" value="default" />
|
|
||||||
<option name="iconsTheme" value="default" />
|
|
||||||
<option name="button1Title" value="" />
|
|
||||||
<option name="button1Url" value="" />
|
|
||||||
<option name="button2Title" value="" />
|
|
||||||
<option name="button2Url" value="" />
|
|
||||||
<option name="customApplicationId" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
13
.idea/material_theme_project_new.xml
generated
13
.idea/material_theme_project_new.xml
generated
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MaterialThemeProjectNewConfig">
|
|
||||||
<option name="metadata">
|
|
||||||
<MTProjectMetadataState>
|
|
||||||
<option name="migrated" value="true" />
|
|
||||||
<option name="pristineConfig" value="false" />
|
|
||||||
<option name="userId" value="-231c7013:18e53d79f11:-8000" />
|
|
||||||
<option name="version" value="8.13.2" />
|
|
||||||
</MTProjectMetadataState>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/sipher.iml" filepath="$PROJECT_DIR$/.idea/sipher.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/sipher.iml
generated
12
.idea/sipher.iml
generated
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
18
.vscode/settings.json
vendored
Normal file
18
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"css.customData": [
|
||||||
|
".vscode/tailwind.json"
|
||||||
|
],
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.wordWrap": "wordWrapColumn",
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.wordWrapColumn": 120,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"typescript.preferences.quoteStyle": "double",
|
||||||
|
"editor.formatOnSaveMode": "file",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
.vscode/tailwind.json
vendored
Normal file
105
.vscode/tailwind.json
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"version": 4.0,
|
||||||
|
"atDirectives": [
|
||||||
|
{
|
||||||
|
"name": "@import",
|
||||||
|
"description": "Use the `@import` directive to inline import CSS files, including Tailwind itself.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#import-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@theme",
|
||||||
|
"description": "Use the `@theme` directive to define your project's custom design tokens, like fonts, colors, and breakpoints.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#theme-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@source",
|
||||||
|
"description": "Use the `@source` directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#source-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@utility",
|
||||||
|
"description": "Use the `@utility` directive to add custom utilities to your project that work with variants like `hover`, `focus` and `lg`.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#utility-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@variant",
|
||||||
|
"description": "Use the `@variant` directive to apply a Tailwind variant to styles in your CSS. If you need to apply multiple variants at the same time, use nesting.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#variant-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@custom-variant",
|
||||||
|
"description": "Use the `@custom-variant` directive to add a custom variant in your project. This lets you write utilities like `pointer-coarse:size-48` and `theme-midnight:bg-slate-900`.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#custom-variant-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@apply",
|
||||||
|
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you need to write custom CSS (like to override the styles in a third-party library) but still want to work with your design tokens and use the same syntax you’re used to using in your HTML.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#apply-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@reference",
|
||||||
|
"description": "If you want to use `@apply` or `@variant` in the `<style>` block of a Vue or Svelte component, or within CSS modules, you will need to import your theme variables, custom utilities, and custom variants to make those values available in that context. To do this without duplicating any CSS in your output, use the `@reference` directive to import your main stylesheet for reference without actually including the styles.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#reference-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@config",
|
||||||
|
"description": "Use the `@config` directive to load a legacy JavaScript-based configuration file.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#config-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@plugin",
|
||||||
|
"description": "Use the `@plugin` directive to load a Javascript-based plugin. The `@plugin` directive accepts either a package name or a local path.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#plugin-directive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
249
README.md
249
README.md
|
|
@ -1,249 +0,0 @@
|
||||||
# Silent Whisper - SiPher
|
|
||||||
|
|
||||||
[//]: # (TODO:)
|
|
||||||
|
|
||||||
### Video Demo: <URL HERE>
|
|
||||||
|
|
||||||
### Description:
|
|
||||||
|
|
||||||
I created this app mainly to learn more about design and improve my skills in this area, plus learn a bit more about how
|
|
||||||
E2EE encryption works.
|
|
||||||
|
|
||||||
I ran into LOTS of problems (like, seriously, a ton) when starting the app, which made me use some workarounds to get it
|
|
||||||
working 100%.
|
|
||||||
|
|
||||||
#### What does it do?
|
|
||||||
|
|
||||||
Here's what it does:
|
|
||||||
|
|
||||||
1. You register your account with just a Username and Password - no email or obvious identification needed
|
|
||||||
2. You share your SUUID with another user who then requests consent to start a chat
|
|
||||||
3. Once a chat starts, you can send messages to that user, following this flow:
|
|
||||||
- You send a message
|
|
||||||
- It gets encrypted using RSA-OAEP with SHA-256 and then encoded in Base64 format
|
|
||||||
- It's sent to the server, stored in the database, and then triggers Supabase's Realtime to update both chats in
|
|
||||||
real-time
|
|
||||||
- Rinse & Repeat
|
|
||||||
|
|
||||||
That's the basic functionality of the app - just encrypting messages and sending them to a server that eventually stores
|
|
||||||
them and uses a websocket connection (not really sure if it's a websocket, but through debugging, I noticed that at
|
|
||||||
least in development, it uses websocket). Nothing special or functionality that would make the app really secure or
|
|
||||||
ideal for real use.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Design Choices
|
|
||||||
|
|
||||||
#### Tech Stack
|
|
||||||
|
|
||||||
For the tech stack, I decided to use:
|
|
||||||
|
|
||||||
- NextJs - Makes my life easier since Vercel can host it in a free plan
|
|
||||||
- Supabase - Has the Realtime feature, in which Vercel
|
|
||||||
|
|
||||||
And that's it, really. I only used those two to create this app. Along with obviously WebApis that are supported in
|
|
||||||
browsers.
|
|
||||||
|
|
||||||
If curious, though, I use IntelliJ products to code because I like their products.
|
|
||||||
|
|
||||||
#### Front-End
|
|
||||||
|
|
||||||
I had a lot of trouble with the design, mainly because I wanted the app to be pretty, minimalist, and work well enough.
|
|
||||||
|
|
||||||
For the front-end design, I'll admit I used Claude (Anthropic) to make better decisions about the app, such as styling
|
|
||||||
issues (Mainly trying to make it mobile compatible).
|
|
||||||
Even though I used AI for help, I had in mind what I wanted: Similar to WhatsApp. With an empty margin and
|
|
||||||
the app UI smaller than the total browser screen. This really helped make the design cleaner, for some reason.
|
|
||||||
|
|
||||||
I also decided that, in the main design, I wanted to use a more striking color with a deeper color - in this case,
|
|
||||||
orange and black had a great contrast.
|
|
||||||
|
|
||||||
I did use ShadCn to make my life easier since it's a really good library for better development on the front-end. I also
|
|
||||||
considered using bootstrap or other libraries such as MaterialUi, but ShadCn had the easiest setup, was more
|
|
||||||
minimalistic
|
|
||||||
and I could control the components in a better way.
|
|
||||||
|
|
||||||
#### Back-End
|
|
||||||
|
|
||||||
The back-end design was a bit easier to do, thanks to how easy Supabase and NextJS API routes are to use, so there
|
|
||||||
wasn't much debate about this specific part. Even though I had many problems, mainly with RLS policies in Supabase, due
|
|
||||||
to pure lack of experience with it. For a better experience, I also used Supabase's own AI to help debug scripts, drop
|
|
||||||
functions, and request the best approach method for this project.
|
|
||||||
|
|
||||||
I debated myself a lot when making the SQL scripts, though. They changed way too much and probably this has a weird DB
|
|
||||||
structure. First I had in mind that each thread should be "indexable" (meaning, if the thread could be searched or not
|
|
||||||
for joining), then I changed it to each user being indexable or not (meaning a user could search for another using by
|
|
||||||
either using that user's SUUID or username) and I went with that.<br/>
|
|
||||||
Then I had to change the message structure due to forgetting that each message sent should be encrypted for the current
|
|
||||||
user too, else that user wouldn't be able to read what he sent to that user due to that message being encrypted only
|
|
||||||
with
|
|
||||||
the public key of the receiver end. With that, I also had to change the thread structures, making them separate in 3
|
|
||||||
tables:
|
|
||||||
|
|
||||||
- "message_threads" - The main table
|
|
||||||
- "thread_participants" - Holds the participants in each thread by indexing the thread id and
|
|
||||||
user id
|
|
||||||
- "messages" - Holds the messages for both the user that sent them (By encrypting that message with the user's own
|
|
||||||
public
|
|
||||||
key for access) and the receiver. The front-end can differenciate between the sender/receiver by using the key "
|
|
||||||
sender_uuid"
|
|
||||||
and comparing the logged user's uuid with that key. Each message is indexed to the thread_id for retrieval
|
|
||||||
|
|
||||||
The main issue I did run into was: Supabase does not support username-only login.<br/>
|
|
||||||
So I had to improvise. I have a few domains that I bought some years ago and set the app to use that domain as a false
|
|
||||||
e-mail:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const domain = process.env.DOMAIN;
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Server is misconfigured, please check env variables and try again."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500
|
|
||||||
})
|
|
||||||
} else if (!username || !password || !public_key) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Missing params"
|
|
||||||
}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
// First create the auth user
|
|
||||||
const {data: {user}, error: authError} = await supabase.auth.signUp({
|
|
||||||
email: `${username}@${domain}`, // Using username as email
|
|
||||||
password: password,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
This function represents the register, but the login-flow also works in a similar way, you can check
|
|
||||||
its [script](./src/app/api/auth/login/route.ts) too.
|
|
||||||
|
|
||||||
Is this a breach on their policy? Well, I don't think it is... At least I hope it isn't.
|
|
||||||
|
|
||||||
But this works when setting a username-only login without having too much trouble.
|
|
||||||
|
|
||||||
Also, here's a cool badge:
|
|
||||||
|
|
||||||
[](https://wakatime.com/badge/user/e0979afa-f854-452d-b8a8-56f9d69eaa3b/project/eea66021-88c7-4467-8434-937fabc8149a)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
##### Team MVPs
|
|
||||||
|
|
||||||
By team MVPs, I mean the functions that took the most work and time to get done and finished to a state where they
|
|
||||||
worked well enough (as far as I could test).
|
|
||||||
|
|
||||||
1. [CryptoManager](./src/lib/crypto/keys.ts)
|
|
||||||
|
|
||||||
This function really gave me A LOT of headaches, seriously, A LOT of headaches.
|
|
||||||
|
|
||||||
Starting with how the encryption would work, I first thought of something like PGP, but it would be VERY long and
|
|
||||||
possibly conflict with Supabase when storing it since I didn't know how it would handle a very long context. I admit
|
|
||||||
I asked Claude for help to decide the best method for this situation, and I still feel it's not as secure as I
|
|
||||||
wanted, but it works perfectly and isn't too complex.
|
|
||||||
|
|
||||||
Another important point that I decided on design-wise is that both users would need to have the same message
|
|
||||||
encrypted 2x. One from who sent it using their own public key (So that user can read their own message) and one for
|
|
||||||
who will receive it using that user's public key (So they can also read the received message).
|
|
||||||
|
|
||||||
Here are the key functions with detailed explanations:
|
|
||||||
<br/><br/>
|
|
||||||
`static async generateUserKeys(): Promise<CryptoKeyPair>`:
|
|
||||||
Generates a private and public key when called
|
|
||||||
<br/><br/>
|
|
||||||
`static async storePrivateKey(privateKey: CryptoKey): Promise<void>`:
|
|
||||||
Stores the private key in the "IndexedDB" database
|
|
||||||
<br/><br/>
|
|
||||||
`static async deletePrivateKey(): Promise<void>`:
|
|
||||||
Deletes the previously recorded private key. If there isn't one, returns an error.
|
|
||||||
<br/><br/>
|
|
||||||
`static async getPrivateKey(): Promise<CryptoKey | null>`:
|
|
||||||
Returns the user's current key for message decryption. Returns "null" if there isn't a key
|
|
||||||
<br/><br/>
|
|
||||||
`static async prepareAndSendMessage(message: string, senderPublicKey: JsonWebKey, recipientPublicKey: JsonWebKey, threadId: string): Promise<void>`:
|
|
||||||
Prepares the message for both users using the "encryptMessage" method, and then sends it to the "
|
|
||||||
/api/user/send/message" API that invokes the SQL function in Supabase
|
|
||||||
<br/><br/>
|
|
||||||
`static async decryptThreadMessages(messages: any[], userUuid: string): Promise<SiPher.DecryptedMessage[]>`:
|
|
||||||
Receives an array of messages (from Supabase's API) and decrypts both the sent and received messages using the
|
|
||||||
current user's private key. For messages that the user themselves sent, decryption is also done using the current
|
|
||||||
user's private key, since it was encrypted for both sender and recipient.
|
|
||||||
<br/><br/>
|
|
||||||
`static async encryptMessage(message: string, recipientPublicKey: JsonWebKey): Promise<string>`:
|
|
||||||
Encrypts a message, returning a base64 encoded string after being encrypted using RSA-OAEP
|
|
||||||
<br/><br/>
|
|
||||||
`static async exportPrivateKey(filename: string = 'private-key-backup'): Promise<{ text: string, file: File } | null>`:
|
|
||||||
Helper function to facilitate the backup of the current private key
|
|
||||||
<br/><br/>
|
|
||||||
`static async validateKeyPair(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise<boolean>`:
|
|
||||||
Validates the current private key with the public key stored in the database by encrypting a message with a
|
|
||||||
timestamp, then trying to decrypt it afterward. Returns a boolean in both cases.
|
|
||||||
<br/><br/>
|
|
||||||
`static async restoreFromBackup(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise<boolean>`:
|
|
||||||
Helper function to restore a backup. Not currently being used.
|
|
||||||
<br/><br/>
|
|
||||||
`private static async openDB(): Promise<IDBDatabase>`:
|
|
||||||
Private function to open the database connection.
|
|
||||||
|
|
||||||
2. [SQL Functions](./supabase/sql_snippets)
|
|
||||||
|
|
||||||
Seriously, the amount of trouble I had with SQL functions is unreal... Not just functions, but also RLS policies,
|
|
||||||
realtime permissions, etc. I had to ask for help from Supabase AI (and a bit from Claude, since honestly,
|
|
||||||
Supabase's doesn't give as much explanation for corrections and other stuff).
|
|
||||||
|
|
||||||
The main functions are:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE FUNCTION public.create_private_thread(participant_suuid TEXT) RETURNS UUID
|
|
||||||
```
|
|
||||||
Creates a private thread by getting the current user suuid (current_user_suuid) and the target user, checks if
|
|
||||||
there's already a thread with those 2 participants and creates one if there isn't or returns an existing thread
|
|
||||||
id
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_thread(thread_uuid UUID, user_id UUID)
|
|
||||||
```
|
|
||||||
Retrieves a thread using its uuid along with the user_id. If found, returns the thread information (thread_id,
|
|
||||||
participants, participants_suuids, messages). If the thread doesn't exist, returns an empty value.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_user_threads(user_id UUID)
|
|
||||||
```
|
|
||||||
Retrieves a user's threads using their own uuid, returning an array of existing threads
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE FUNCTION public.send_message(
|
|
||||||
thread_uuid UUID,
|
|
||||||
sender_content TEXT,
|
|
||||||
recipient_content TEXT
|
|
||||||
) RETURNS UUID
|
|
||||||
```
|
|
||||||
Inserts both users' messages into the database, both encrypted with their respective keys
|
|
||||||
|
|
||||||
It's totally possible I forgot some functions or that others were deleted during development, so I included all
|
|
||||||
the functions made, along with RLS policies and triggers.
|
|
||||||
Some functions weren't mentioned because they weren't as problematic to make. There is also a high possibility of
|
|
||||||
this app being really insecure since I am not too familiar with SQL (I always preferred NoSQL dbs.)
|
|
||||||
|
|
||||||
I will not document each page since I don't think it's necessary and that would make this README too long and
|
|
||||||
cluttered.
|
|
||||||
|
|
||||||
I did re-use code of previous projects as inspiration. Mainly the middleware and some other styling (Such as the
|
|
||||||
Sidebar).
|
|
||||||
|
|
||||||
I did not mention any API because the API routes mainly use supabase's functions to work, so I do not think it is
|
|
||||||
necessary to mention them here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For clarification, I did use AI to help me on this project:
|
|
||||||
|
|
||||||
- Claude - Helped with NextJs and React debugging (I don't know how to read the errors on react, sometimes it just
|
|
||||||
outputs a simple message without explicit details on where the error happened), helping on some SQL functions too (
|
|
||||||
Mainly RLS issues on realtime). Also helped when I couldn't really fix the style of some components.
|
|
||||||
- Supabase's AI - I don't think it helped that much since, honestly, I don't think it's quite good at the purpose it was
|
|
||||||
made to serve. Might be a skill issue on my part though. It helped mainly in debugging of some scripts that weren't
|
|
||||||
working properly, since Supabase does not really support logs (at least, I never found where to look at)
|
|
||||||
|
|
||||||
You can check it out by using this link: https://sipher.space
|
|
||||||
670
bun.lock
Normal file
670
bun.lock
Normal file
|
|
@ -0,0 +1,670 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "sipher",
|
||||||
|
"dependencies": {
|
||||||
|
"@convex-dev/better-auth": "^0.9.7",
|
||||||
|
"@marsidev/react-turnstile": "^1.3.1",
|
||||||
|
"@nanostores/react": "^1.0.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@types/libsodium-wrappers": "^0.7.14",
|
||||||
|
"better-auth": "1.3.34",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"convex": "^1.29.3",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"dexie": "^4.2.1",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
|
"libsodium-wrappers": "^0.7.15",
|
||||||
|
"lucide-react": "^0.555.0",
|
||||||
|
"nanostores": "^1.1.0",
|
||||||
|
"next": "16.0.4",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-day-picker": "^9.11.2",
|
||||||
|
"react-dom": "19.2.0",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"ws": "^8.18.3",
|
||||||
|
"zod": "^4.1.13",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/bun": "^1.3.3",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.3.34", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-rt/Bgl0Xa8OQ2DUMKCZEJ8vL9kUw4NCJsBP9Sj9uRhbsK8NEMPiznUOFMkUY2FvrslvfKN7H/fivwyHz9c7HzQ=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.3.34", "", { "dependencies": { "@better-auth/core": "1.3.34", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" } }, "sha512-aQZ3wN90YMqV49diWxAMe1k7s2qb55KCsedCZne5PlgCjU4s3YtnqyjC5FEpzw2KY8l8rvR7DMAsDl13NjObKA=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||||
|
|
||||||
|
"@convex-dev/better-auth": ["@convex-dev/better-auth@0.9.7", "", { "dependencies": { "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^3.24.4" }, "peerDependencies": { "better-auth": "1.3.27", "convex": ">=1.28.2 <1.35.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-ni0oLM3IQho8KVBlMoyTk50IIbckhZmlEMxLgaVSixKmFJ4N/kGC6T91MjPTw3+bVLn/qHmIinLp7Dm+NRYzBw=="],
|
||||||
|
|
||||||
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
||||||
|
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||||
|
|
||||||
|
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
|
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
|
||||||
|
|
||||||
|
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||||
|
|
||||||
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||||
|
|
||||||
|
"@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.3.1", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-h2THG/75k4Y049hgjSGPIcajxXnh+IZAiXVbryQyVmagkboN7pJtBgR16g8akjwUBSfRrg6jw6KvPDjscQflog=="],
|
||||||
|
|
||||||
|
"@nanostores/react": ["@nanostores/react@1.0.0", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0", "react": ">=18.0.0" } }, "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A=="],
|
||||||
|
|
||||||
|
"@next/env": ["@next/env@16.0.4", "", {}, "sha512-FDPaVoB1kYhtOz6Le0Jn2QV7RZJ3Ngxzqri7YX4yu3Ini+l5lciR7nA9eNDpKTmDm7LWZtxSju+/CQnwRBn2pA=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TN0cfB4HT2YyEio9fLwZY33J+s+vMIgC84gQCOLZOYusW7ptgjIn8RwxQt0BUpoo9XRRVVWEHLld0uhyux1ZcA=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XsfI23jvimCaA7e+9f3yMCoVjrny2D11G6H8NCcgv+Ina/TQhKPXB9P4q0WjTuEoyZmcNvPdrZ+XtTh3uPfH7Q=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-uo8X7qHDy4YdJUhaoJDMAbL8VT5Ed3lijip2DdBHIB4tfKAvB1XBih6INH2L4qIi4jA0Qq1J0ErxcOocBmUSwg=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvR/AjNIAxsIz0PCNcZYpH+WmNIKNLcL4XYEfo+ArDi7GsxKWFO5BvVBLXbhti8Coyv3DE983NsitzUsGH5yTw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2hebpsd5MRRtgqmT7Jj/Wze+wG+ZEXUK2KFFL4IlZ0amEEFADo4ywsifJNeFTQGsamH3/aXkKWymDvgEi+pc2Q=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-pzRXf0LZZ8zMljH78j8SeLncg9ifIOp3ugAFka+Bq8qMzw6hPXOc7wydY7ardIELlczzzreahyTpwsim/WL3Sg=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-7G/yJVzum52B5HOqqbQYX9bJHkN+c4YyZ2AIvEssMHQlbAWOn3iIJjD4sM6ihWsBxuljiTKJovEYlD1K8lCUHw=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-0Vy4g8SSeVkuU89g2OFHqGKM4rxsQtihGfenjx2tRckPrge5+gtFnRWGAAwvGXr0ty3twQvcnYjEyOrLHJ4JWA=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "@peculiar/asn1-x509-attr": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.6.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-pkcs8": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.6.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-pfx": "^2.6.0", "@peculiar/asn1-pkcs8": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "@peculiar/asn1-x509-attr": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA=="],
|
||||||
|
|
||||||
|
"@peculiar/x509": ["@peculiar/x509@1.14.2", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.8", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
|
||||||
|
|
||||||
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="],
|
||||||
|
|
||||||
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
|
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
|
|
||||||
|
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
|
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
|
||||||
|
|
||||||
|
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||||
|
|
||||||
|
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.3.34", "", { "dependencies": { "@better-auth/core": "1.3.34", "@better-auth/telemetry": "1.3.34", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-LWA52SlvnUBJRbN8VLSTLILPomZY3zZAiLxVJCeSQ5uVmaIKkMBhERitkfJcXB9RJcfl4uP+3EqKkb6hX1/uiw=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
|
||||||
|
|
||||||
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||||
|
|
||||||
|
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
||||||
|
|
||||||
|
"convex": ["convex@1.29.3", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-tg5TXzMjpNk9m50YRtdp6US+t7ckxE4E+7DNKUCjJ2MupQs2RBSPF/z5SNN4GUmQLSfg0eMILDySzdAvjTrhnw=="],
|
||||||
|
|
||||||
|
"convex-helpers": ["convex-helpers@0.1.106", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.25.4", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-hWRe3yDaAVHMe4CUYw1YoQLiPZ1KIx6Kbf0w6UcRDx1BXpJgMCl3GVIMiSeYiA0PkbwjnIwGWIvoUVKloG5Tyw=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
|
"cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
|
"dexie": ["dexie@4.2.1", "", {}, "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="],
|
||||||
|
|
||||||
|
"engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="],
|
||||||
|
|
||||||
|
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
|
||||||
|
|
||||||
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||||
|
|
||||||
|
"framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.2", "", {}, "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
|
||||||
|
|
||||||
|
"libsodium": ["libsodium@0.7.15", "", {}, "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw=="],
|
||||||
|
|
||||||
|
"libsodium-wrappers": ["libsodium-wrappers@0.7.15", "", { "dependencies": { "libsodium": "^0.7.15" } }, "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
|
||||||
|
|
||||||
|
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
|
"next": ["next@16.0.4", "", { "dependencies": { "@next/env": "16.0.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.4", "@next/swc-darwin-x64": "16.0.4", "@next/swc-linux-arm64-gnu": "16.0.4", "@next/swc-linux-arm64-musl": "16.0.4", "@next/swc-linux-x64-gnu": "16.0.4", "@next/swc-linux-x64-musl": "16.0.4", "@next/swc-win32-arm64-msvc": "16.0.4", "@next/swc-win32-x64-msvc": "16.0.4", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-vICcxKusY8qW7QFOzTvnRL1ejz2ClTqDKtm1AcUjm2mPv/lVAdgpGNsftsPRIDJOXOjRQO68i1dM8Lp8GZnqoA=="],
|
||||||
|
|
||||||
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
|
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||||
|
|
||||||
|
"react-day-picker": ["react-day-picker@9.11.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-TD/xMUGg2oiKX8jUR21MST5pj+7Y36097YtnDHQFlIcZOu3mbLLw2B2JqEByEGrR3HHveWYnKlyls6WqJgohAg=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||||
|
|
||||||
|
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||||
|
|
||||||
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||||
|
|
||||||
|
"remeda": ["remeda@2.32.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-BZx9DsT4FAgXDTOdgJIc5eY6ECIXMwtlSPQoPglF20ycSWigttDDe88AozEsPPT4OWk5NujroGSBC1phw5uU+w=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="],
|
||||||
|
|
||||||
|
"socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="],
|
||||||
|
|
||||||
|
"socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="],
|
||||||
|
|
||||||
|
"socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
|
||||||
|
|
||||||
|
"tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||||
|
|
||||||
|
"@convex-dev/better-auth/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="],
|
||||||
|
|
||||||
|
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
|
"engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
|
"socket.io-adapter/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
|
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,13 @@
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
|
|
@ -17,5 +18,5 @@
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"registries": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1013
convex/_generated/api.d.ts
vendored
Normal file
1013
convex/_generated/api.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
23
convex/_generated/api.js
Normal file
23
convex/_generated/api.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api = anyApi;
|
||||||
|
export const internal = anyApi;
|
||||||
|
export const components = componentsGeneric();
|
||||||
58
convex/_generated/dataModel.d.ts
vendored
Normal file
58
convex/_generated/dataModel.d.ts
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated data model types.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AnyDataModel } from "convex/server";
|
||||||
|
import type { GenericId } from "convex/values";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No `schema.ts` file found!
|
||||||
|
*
|
||||||
|
* This generated code has permissive types like `Doc = any` because
|
||||||
|
* Convex doesn't know your schema. If you'd like more type safety, see
|
||||||
|
* https://docs.convex.dev/using/schemas for instructions on how to add a
|
||||||
|
* schema file.
|
||||||
|
*
|
||||||
|
* After you change a schema, rerun codegen with `npx convex dev`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names of all of your Convex tables.
|
||||||
|
*/
|
||||||
|
export type TableNames = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a document stored in Convex.
|
||||||
|
*/
|
||||||
|
export type Doc = any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identifier for a document in Convex.
|
||||||
|
*
|
||||||
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
|
*
|
||||||
|
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||||
|
*
|
||||||
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
|
* strings when type checking.
|
||||||
|
*/
|
||||||
|
export type Id<TableName extends TableNames = TableNames> =
|
||||||
|
GenericId<TableName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type describing your Convex data model.
|
||||||
|
*
|
||||||
|
* This type includes information about what tables you have, the type of
|
||||||
|
* documents stored in those tables, and the indexes defined on them.
|
||||||
|
*
|
||||||
|
* This type is used to parameterize methods like `queryGeneric` and
|
||||||
|
* `mutationGeneric` to make them type-safe.
|
||||||
|
*/
|
||||||
|
export type DataModel = AnyDataModel;
|
||||||
143
convex/_generated/server.d.ts
vendored
Normal file
143
convex/_generated/server.d.ts
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionBuilder,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex query functions.
|
||||||
|
*
|
||||||
|
* The query context is passed as the first argument to any Convex query
|
||||||
|
* function run on the server.
|
||||||
|
*
|
||||||
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
|
* read-only.
|
||||||
|
*/
|
||||||
|
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex mutation functions.
|
||||||
|
*
|
||||||
|
* The mutation context is passed as the first argument to any Convex mutation
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex action functions.
|
||||||
|
*
|
||||||
|
* The action context is passed as the first argument to any Convex action
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from the database within Convex query functions.
|
||||||
|
*
|
||||||
|
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||||
|
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||||
|
* building a query.
|
||||||
|
*/
|
||||||
|
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from and write to the database within Convex mutation
|
||||||
|
* functions.
|
||||||
|
*
|
||||||
|
* Convex guarantees that all writes within a single mutation are
|
||||||
|
* executed atomically, so you never have to worry about partial writes leaving
|
||||||
|
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||||
|
* for the guarantees Convex provides your functions.
|
||||||
|
*/
|
||||||
|
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||||
93
convex/_generated/server.js
Normal file
93
convex/_generated/server.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const query = queryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalQuery = internalQueryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const mutation = mutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalMutation = internalMutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const action = actionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalAction = internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export const httpAction = httpActionGeneric;
|
||||||
8
convex/auth.config.ts
Normal file
8
convex/auth.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
domain: process.env.CONVEX_SITE_URL,
|
||||||
|
applicationID: "convex",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
60
convex/auth.ts
Normal file
60
convex/auth.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
||||||
|
import { convex } from "@convex-dev/better-auth/plugins";
|
||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { captcha, username } from "better-auth/plugins";
|
||||||
|
import { components } from "./_generated/api";
|
||||||
|
import { DataModel } from "./_generated/dataModel";
|
||||||
|
import { query } from "./_generated/server";
|
||||||
|
import authSchema from "./betterAuth/schema";
|
||||||
|
|
||||||
|
const siteUrl = process.env.SITE_URL!;
|
||||||
|
|
||||||
|
// The component client has methods needed for integrating Convex with Better Auth,
|
||||||
|
// as well as helper methods for general use.
|
||||||
|
export const authComponent = createClient<DataModel, typeof authSchema>(
|
||||||
|
components.betterAuth,
|
||||||
|
{
|
||||||
|
local: {
|
||||||
|
schema: authSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createAuth = (
|
||||||
|
ctx: GenericCtx<DataModel>,
|
||||||
|
{ optionsOnly } = { optionsOnly: false },
|
||||||
|
) => {
|
||||||
|
return betterAuth({
|
||||||
|
logger: {
|
||||||
|
disabled: optionsOnly,
|
||||||
|
},
|
||||||
|
baseURL: siteUrl,
|
||||||
|
database: authComponent.adapter(ctx),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
convex(),
|
||||||
|
captcha({
|
||||||
|
provider: "cloudflare-turnstile",
|
||||||
|
secretKey: process.env.CAPTCHA_SECRET_KEY!,
|
||||||
|
}),
|
||||||
|
username({
|
||||||
|
displayUsernameValidator: (displayUsername) => {
|
||||||
|
// Allow only alphanumeric characters, underscores, and hyphens
|
||||||
|
return /^[a-zA-Z0-9_-]+$/.test(displayUsername)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example function for getting the current user
|
||||||
|
// Feel free to edit, omit, etc.
|
||||||
|
export const getCurrentUser = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return authComponent.getAuthUser(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
52
convex/betterAuth/_generated/api.ts
Normal file
52
convex/betterAuth/_generated/api.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as adapter from "../adapter.js";
|
||||||
|
import type * as auth from "../auth.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiFromModules,
|
||||||
|
FilterApi,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
const fullApi: ApiFromModules<{
|
||||||
|
adapter: typeof adapter;
|
||||||
|
auth: typeof auth;
|
||||||
|
}> = anyApi as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's public API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api: FilterApi<
|
||||||
|
typeof fullApi,
|
||||||
|
FunctionReference<any, "public">
|
||||||
|
> = anyApi as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's internal API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = internal.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const internal: FilterApi<
|
||||||
|
typeof fullApi,
|
||||||
|
FunctionReference<any, "internal">
|
||||||
|
> = anyApi as any;
|
||||||
|
|
||||||
|
export const components = componentsGeneric() as unknown as {};
|
||||||
992
convex/betterAuth/_generated/component.ts
Normal file
992
convex/betterAuth/_generated/component.ts
Normal file
|
|
@ -0,0 +1,992 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `ComponentApi` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FunctionReference } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing a Convex component's exposed API.
|
||||||
|
*
|
||||||
|
* Useful when expecting a parameter like `components.myComponent`.
|
||||||
|
* Usage:
|
||||||
|
* ```ts
|
||||||
|
* async function myFunction(ctx: QueryCtx, component: ComponentApi) {
|
||||||
|
* return ctx.runQuery(component.someFile.someQuery, { ...args });
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
|
{
|
||||||
|
adapter: {
|
||||||
|
create: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
displayUsername?: null | string;
|
||||||
|
email: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
image?: null | string;
|
||||||
|
name: string;
|
||||||
|
updatedAt: number;
|
||||||
|
userId?: null | string;
|
||||||
|
username?: null | string;
|
||||||
|
};
|
||||||
|
model: "user";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number;
|
||||||
|
ipAddress?: null | string;
|
||||||
|
token: string;
|
||||||
|
updatedAt: number;
|
||||||
|
userAgent?: null | string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "session";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
accessToken?: null | string;
|
||||||
|
accessTokenExpiresAt?: null | number;
|
||||||
|
accountId: string;
|
||||||
|
createdAt: number;
|
||||||
|
idToken?: null | string;
|
||||||
|
password?: null | string;
|
||||||
|
providerId: string;
|
||||||
|
refreshToken?: null | string;
|
||||||
|
refreshTokenExpiresAt?: null | number;
|
||||||
|
scope?: null | string;
|
||||||
|
updatedAt: number;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "account";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number;
|
||||||
|
identifier: string;
|
||||||
|
updatedAt: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
model: "verification";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
model: "jwks";
|
||||||
|
};
|
||||||
|
onCreateHandle?: string;
|
||||||
|
select?: Array<string>;
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
deleteMany: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
model: "user";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "emailVerified"
|
||||||
|
| "image"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "userId"
|
||||||
|
| "username"
|
||||||
|
| "displayUsername"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "session";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "expiresAt"
|
||||||
|
| "token"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "ipAddress"
|
||||||
|
| "userAgent"
|
||||||
|
| "userId"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "account";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "accountId"
|
||||||
|
| "providerId"
|
||||||
|
| "userId"
|
||||||
|
| "accessToken"
|
||||||
|
| "refreshToken"
|
||||||
|
| "idToken"
|
||||||
|
| "accessTokenExpiresAt"
|
||||||
|
| "refreshTokenExpiresAt"
|
||||||
|
| "scope"
|
||||||
|
| "password"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "verification";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "identifier"
|
||||||
|
| "value"
|
||||||
|
| "expiresAt"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "jwks";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "publicKey" | "privateKey" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
onDeleteHandle?: string;
|
||||||
|
paginationOpts: {
|
||||||
|
cursor: string | null;
|
||||||
|
endCursor?: string | null;
|
||||||
|
id?: number;
|
||||||
|
maximumBytesRead?: number;
|
||||||
|
maximumRowsRead?: number;
|
||||||
|
numItems: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
deleteOne: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
model: "user";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "emailVerified"
|
||||||
|
| "image"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "userId"
|
||||||
|
| "username"
|
||||||
|
| "displayUsername"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "session";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "expiresAt"
|
||||||
|
| "token"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "ipAddress"
|
||||||
|
| "userAgent"
|
||||||
|
| "userId"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "account";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "accountId"
|
||||||
|
| "providerId"
|
||||||
|
| "userId"
|
||||||
|
| "accessToken"
|
||||||
|
| "refreshToken"
|
||||||
|
| "idToken"
|
||||||
|
| "accessTokenExpiresAt"
|
||||||
|
| "refreshTokenExpiresAt"
|
||||||
|
| "scope"
|
||||||
|
| "password"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "verification";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "identifier"
|
||||||
|
| "value"
|
||||||
|
| "expiresAt"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "jwks";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "publicKey" | "privateKey" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
onDeleteHandle?: string;
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
findMany: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
limit?: number;
|
||||||
|
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||||
|
offset?: number;
|
||||||
|
paginationOpts: {
|
||||||
|
cursor: string | null;
|
||||||
|
endCursor?: string | null;
|
||||||
|
id?: number;
|
||||||
|
maximumBytesRead?: number;
|
||||||
|
maximumRowsRead?: number;
|
||||||
|
numItems: number;
|
||||||
|
};
|
||||||
|
sortBy?: { direction: "asc" | "desc"; field: string };
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: string;
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
findOne: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||||
|
select?: Array<string>;
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: string;
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
updateMany: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
model: "user";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
displayUsername?: null | string;
|
||||||
|
email?: string;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
image?: null | string;
|
||||||
|
name?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: null | string;
|
||||||
|
username?: null | string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "emailVerified"
|
||||||
|
| "image"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "userId"
|
||||||
|
| "username"
|
||||||
|
| "displayUsername"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "session";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ipAddress?: null | string;
|
||||||
|
token?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userAgent?: null | string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "expiresAt"
|
||||||
|
| "token"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "ipAddress"
|
||||||
|
| "userAgent"
|
||||||
|
| "userId"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "account";
|
||||||
|
update: {
|
||||||
|
accessToken?: null | string;
|
||||||
|
accessTokenExpiresAt?: null | number;
|
||||||
|
accountId?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
idToken?: null | string;
|
||||||
|
password?: null | string;
|
||||||
|
providerId?: string;
|
||||||
|
refreshToken?: null | string;
|
||||||
|
refreshTokenExpiresAt?: null | number;
|
||||||
|
scope?: null | string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "accountId"
|
||||||
|
| "providerId"
|
||||||
|
| "userId"
|
||||||
|
| "accessToken"
|
||||||
|
| "refreshToken"
|
||||||
|
| "idToken"
|
||||||
|
| "accessTokenExpiresAt"
|
||||||
|
| "refreshTokenExpiresAt"
|
||||||
|
| "scope"
|
||||||
|
| "password"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "verification";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
identifier?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "identifier"
|
||||||
|
| "value"
|
||||||
|
| "expiresAt"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "jwks";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
privateKey?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "publicKey" | "privateKey" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
onUpdateHandle?: string;
|
||||||
|
paginationOpts: {
|
||||||
|
cursor: string | null;
|
||||||
|
endCursor?: string | null;
|
||||||
|
id?: number;
|
||||||
|
maximumBytesRead?: number;
|
||||||
|
maximumRowsRead?: number;
|
||||||
|
numItems: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
updateOne: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
model: "user";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
displayUsername?: null | string;
|
||||||
|
email?: string;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
image?: null | string;
|
||||||
|
name?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: null | string;
|
||||||
|
username?: null | string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "emailVerified"
|
||||||
|
| "image"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "userId"
|
||||||
|
| "username"
|
||||||
|
| "displayUsername"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "session";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ipAddress?: null | string;
|
||||||
|
token?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userAgent?: null | string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "expiresAt"
|
||||||
|
| "token"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "ipAddress"
|
||||||
|
| "userAgent"
|
||||||
|
| "userId"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "account";
|
||||||
|
update: {
|
||||||
|
accessToken?: null | string;
|
||||||
|
accessTokenExpiresAt?: null | number;
|
||||||
|
accountId?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
idToken?: null | string;
|
||||||
|
password?: null | string;
|
||||||
|
providerId?: string;
|
||||||
|
refreshToken?: null | string;
|
||||||
|
refreshTokenExpiresAt?: null | number;
|
||||||
|
scope?: null | string;
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "accountId"
|
||||||
|
| "providerId"
|
||||||
|
| "userId"
|
||||||
|
| "accessToken"
|
||||||
|
| "refreshToken"
|
||||||
|
| "idToken"
|
||||||
|
| "accessTokenExpiresAt"
|
||||||
|
| "refreshTokenExpiresAt"
|
||||||
|
| "scope"
|
||||||
|
| "password"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "verification";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
identifier?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "identifier"
|
||||||
|
| "value"
|
||||||
|
| "expiresAt"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "jwks";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
privateKey?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "publicKey" | "privateKey" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
onUpdateHandle?: string;
|
||||||
|
},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
60
convex/betterAuth/_generated/dataModel.ts
Normal file
60
convex/betterAuth/_generated/dataModel.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated data model types.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DataModelFromSchemaDefinition,
|
||||||
|
DocumentByName,
|
||||||
|
TableNamesInDataModel,
|
||||||
|
SystemTableNames,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { GenericId } from "convex/values";
|
||||||
|
import schema from "../schema.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names of all of your Convex tables.
|
||||||
|
*/
|
||||||
|
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a document stored in Convex.
|
||||||
|
*
|
||||||
|
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||||
|
*/
|
||||||
|
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||||
|
DataModel,
|
||||||
|
TableName
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identifier for a document in Convex.
|
||||||
|
*
|
||||||
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
|
*
|
||||||
|
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||||
|
*
|
||||||
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
|
* strings when type checking.
|
||||||
|
*
|
||||||
|
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||||
|
*/
|
||||||
|
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||||
|
GenericId<TableName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type describing your Convex data model.
|
||||||
|
*
|
||||||
|
* This type includes information about what tables you have, the type of
|
||||||
|
* documents stored in those tables, and the indexes defined on them.
|
||||||
|
*
|
||||||
|
* This type is used to parameterize methods like `queryGeneric` and
|
||||||
|
* `mutationGeneric` to make them type-safe.
|
||||||
|
*/
|
||||||
|
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||||
161
convex/betterAuth/_generated/server.ts
Normal file
161
convex/betterAuth/_generated/server.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ActionBuilder,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
} from "convex/server";
|
||||||
|
import {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalQuery: QueryBuilder<DataModel, "internal"> =
|
||||||
|
internalQueryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const mutation: MutationBuilder<DataModel, "public"> = mutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalMutation: MutationBuilder<DataModel, "internal"> =
|
||||||
|
internalMutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const action: ActionBuilder<DataModel, "public"> = actionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalAction: ActionBuilder<DataModel, "internal"> =
|
||||||
|
internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
||||||
|
|
||||||
|
type GenericCtx =
|
||||||
|
| GenericActionCtx<DataModel>
|
||||||
|
| GenericMutationCtx<DataModel>
|
||||||
|
| GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex query functions.
|
||||||
|
*
|
||||||
|
* The query context is passed as the first argument to any Convex query
|
||||||
|
* function run on the server.
|
||||||
|
*
|
||||||
|
* If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead.
|
||||||
|
*/
|
||||||
|
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex mutation functions.
|
||||||
|
*
|
||||||
|
* The mutation context is passed as the first argument to any Convex mutation
|
||||||
|
* function run on the server.
|
||||||
|
*
|
||||||
|
* If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead.
|
||||||
|
*/
|
||||||
|
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex action functions.
|
||||||
|
*
|
||||||
|
* The action context is passed as the first argument to any Convex action
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from the database within Convex query functions.
|
||||||
|
*
|
||||||
|
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||||
|
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||||
|
* building a query.
|
||||||
|
*/
|
||||||
|
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from and write to the database within Convex mutation
|
||||||
|
* functions.
|
||||||
|
*
|
||||||
|
* Convex guarantees that all writes within a single mutation are
|
||||||
|
* executed atomically, so you never have to worry about partial writes leaving
|
||||||
|
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||||
|
* for the guarantees Convex provides your functions.
|
||||||
|
*/
|
||||||
|
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||||
13
convex/betterAuth/adapter.ts
Normal file
13
convex/betterAuth/adapter.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { createApi } from "@convex-dev/better-auth";
|
||||||
|
import { createAuth } from "../auth";
|
||||||
|
import schema from "./schema";
|
||||||
|
|
||||||
|
export const {
|
||||||
|
create,
|
||||||
|
findOne,
|
||||||
|
findMany,
|
||||||
|
updateOne,
|
||||||
|
updateMany,
|
||||||
|
deleteOne,
|
||||||
|
deleteMany,
|
||||||
|
} = createApi(schema, createAuth);
|
||||||
5
convex/betterAuth/auth.ts
Normal file
5
convex/betterAuth/auth.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { getStaticAuth } from '@convex-dev/better-auth'
|
||||||
|
import { createAuth } from '../auth'
|
||||||
|
|
||||||
|
// Export a static instance for Better Auth schema generation
|
||||||
|
export const auth = getStaticAuth(createAuth)
|
||||||
5
convex/betterAuth/convex.config.ts
Normal file
5
convex/betterAuth/convex.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineComponent } from "convex/server";
|
||||||
|
|
||||||
|
const component = defineComponent("betterAuth");
|
||||||
|
|
||||||
|
export default component;
|
||||||
73
convex/betterAuth/schema.ts
Normal file
73
convex/betterAuth/schema.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
// This file is auto-generated. Do not edit this file manually.
|
||||||
|
// To regenerate the schema, run:
|
||||||
|
// `npx @better-auth/cli generate --output undefined -y`
|
||||||
|
|
||||||
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export const tables = {
|
||||||
|
user: defineTable({
|
||||||
|
name: v.string(),
|
||||||
|
email: v.string(),
|
||||||
|
emailVerified: v.boolean(),
|
||||||
|
image: v.optional(v.union(v.null(), v.string())),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
userId: v.optional(v.union(v.null(), v.string())),
|
||||||
|
username: v.optional(v.union(v.null(), v.string())),
|
||||||
|
displayUsername: v.optional(v.union(v.null(), v.string())),
|
||||||
|
})
|
||||||
|
.index("email_name", ["email", "name"])
|
||||||
|
.index("name", ["name"])
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("username", ["username"]),
|
||||||
|
session: defineTable({
|
||||||
|
expiresAt: v.number(),
|
||||||
|
token: v.string(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
ipAddress: v.optional(v.union(v.null(), v.string())),
|
||||||
|
userAgent: v.optional(v.union(v.null(), v.string())),
|
||||||
|
userId: v.string(),
|
||||||
|
})
|
||||||
|
.index("expiresAt", ["expiresAt"])
|
||||||
|
.index("expiresAt_userId", ["expiresAt", "userId"])
|
||||||
|
.index("token", ["token"])
|
||||||
|
.index("userId", ["userId"]),
|
||||||
|
account: defineTable({
|
||||||
|
accountId: v.string(),
|
||||||
|
providerId: v.string(),
|
||||||
|
userId: v.string(),
|
||||||
|
accessToken: v.optional(v.union(v.null(), v.string())),
|
||||||
|
refreshToken: v.optional(v.union(v.null(), v.string())),
|
||||||
|
idToken: v.optional(v.union(v.null(), v.string())),
|
||||||
|
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||||
|
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||||
|
scope: v.optional(v.union(v.null(), v.string())),
|
||||||
|
password: v.optional(v.union(v.null(), v.string())),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("accountId", ["accountId"])
|
||||||
|
.index("accountId_providerId", ["accountId", "providerId"])
|
||||||
|
.index("providerId_userId", ["providerId", "userId"])
|
||||||
|
.index("userId", ["userId"]),
|
||||||
|
verification: defineTable({
|
||||||
|
identifier: v.string(),
|
||||||
|
value: v.string(),
|
||||||
|
expiresAt: v.number(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("expiresAt", ["expiresAt"])
|
||||||
|
.index("identifier", ["identifier"]),
|
||||||
|
jwks: defineTable({
|
||||||
|
publicKey: v.string(),
|
||||||
|
privateKey: v.string(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = defineSchema(tables);
|
||||||
|
|
||||||
|
export default schema;
|
||||||
7
convex/convex.config.ts
Normal file
7
convex/convex.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineApp } from "convex/server";
|
||||||
|
import betterAuth from "./betterAuth/convex.config";
|
||||||
|
|
||||||
|
const app = defineApp();
|
||||||
|
app.use(betterAuth);
|
||||||
|
|
||||||
|
export default app;
|
||||||
8
convex/http.ts
Normal file
8
convex/http.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { httpRouter } from "convex/server";
|
||||||
|
import { authComponent, createAuth } from "./auth";
|
||||||
|
|
||||||
|
const http = httpRouter();
|
||||||
|
|
||||||
|
authComponent.registerRoutes(http, createAuth);
|
||||||
|
|
||||||
|
export default http;
|
||||||
|
|
@ -2,6 +2,7 @@ import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
reactCompiler: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
7310
package-lock.json
generated
7310
package-lock.json
generated
File diff suppressed because it is too large
Load diff
86
package.json
86
package.json
|
|
@ -3,48 +3,62 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"start:server": "NODE_ENV=development tsx src/server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.1",
|
"@convex-dev/better-auth": "^0.9.7",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@marsidev/react-turnstile": "^1.3.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@nanostores/react": "^1.0.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@supabase/ssr": "^0.5.2",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@supabase/supabase-js": "^2.47.3",
|
"@types/libsodium-wrappers": "^0.7.14",
|
||||||
"argon2": "^0.41.1",
|
"better-auth": "1.3.34",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.13.5",
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.468.0",
|
"convex": "^1.29.3",
|
||||||
"next": "15.0.4",
|
"cross-env": "^10.1.0",
|
||||||
"next-themes": "^0.4.4",
|
"date-fns": "^4.1.0",
|
||||||
"random-words": "^2.0.1",
|
"dexie": "^4.2.1",
|
||||||
"react": "^19.0.0",
|
"framer-motion": "^12.23.24",
|
||||||
"react-dom": "^19.0.0",
|
"libsodium-wrappers": "^0.7.15",
|
||||||
"tailwind-merge": "^2.5.5",
|
"lucide-react": "^0.555.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"nanostores": "^1.1.0",
|
||||||
|
"next": "16.0.4",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-day-picker": "^9.11.2",
|
||||||
|
"react-dom": "19.2.0",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"ws": "^8.18.3",
|
||||||
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/react": "^19",
|
"@types/bun": "^1.3.3",
|
||||||
"@types/react-dom": "^19",
|
"@types/node": "^24.10.1",
|
||||||
"eslint": "9.16.0",
|
"@types/react": "^19.2.7",
|
||||||
"eslint-config-next": "15.1.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"postcss": "^8",
|
"@types/ws": "^8.18.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"typescript": "^5"
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
11
public/assets/logo/logo-dark.svg
Normal file
11
public/assets/logo/logo-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
38
public/assets/logo/logo-white.svg
Normal file
38
public/assets/logo/logo-white.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Camada_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1024 1024">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #d3d0cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M775.3,448.2c-7.1-39.8-22.1-69-51.4-97.7-4.2-3.8-8.8-8-.3-5.7,5.7,1.6,12.7,4.5,17.9,7.5,3.4,1.8,6,3.6,7.5,3.3,1.2-.5.5-3.1-1-6.8-2.2-4.9-5.6-11.2-8.9-16.4-12.3-20.3-27.7-36-45.5-51.7-6.7-6.1-16.9-14.1-23.7-17.2-3.6-1.8-7.2-3.6-10.9-5-2.8-1.1-6.3-2-3.4-2.7,5.8-.7,21.2.8,28.9,2.5,2.8.2,12.6,3.5,12.5,1.5-5.1-5.8-15.9-10.6-23.4-14.3-10.6-4.8-19.8-8.6-30.7-11-7.1-2-16.8-1.5-23.5-3-2.1-.7,6.7-3.1,11.1-3.6,5.2-.8,10.8-1.3,15.8-1.1,4.5,0,9.3.6,10.4,0,.4-.2.4-.4.2-.8-6.9-5.5-19.6-9-28.5-11.2-13.2-2.8-26-3.6-39.2-3.2-8.1,0-18.5,2.8-26.1,3.9-5,.1,6.7-5.4,8.2-6.1,2.7-1.8,18.1-5.3,9.4-6.2-16.9-1.3-36.3.8-52.6,6.6-6.4,1.9-13.5,7-19,9.9-.1,0-.2,0-.1,0,0-.5,3.9-4.3,7.3-7.2,2.1-1.8,4.3-3.5,6.4-4.9,8.2-5.4,9.3-6.8.4-4.7-9.1,2.5-18,5.6-26.1,10-5.7,2.8-11.1,7.6-16.3,10.6-3.5.9,1.7-5.9,2.4-7.1,4.3-6.2-13.7,3.8-14.9,5.1-12,8.3-23.4,16.8-34.3,26.3-14.2,12.5-11.7,14.9-32.2,14.9-20.7,0-40.2,2.7-60.8,6.9-34.8,7.7-68.1,22.4-92.1,51.5-7.2,9.7-15.4,25.9-19.8,37.7-.6,1.6-1.3,3.2-1.7,4.5-.2.5,0,.7.6.6l2.4-.4c2.2-.3,4.1-.5,6.4-.8,5.7-.7,9.8-1.1,16.1-1.7,27.4-2.3,52.8-5,81.8-2.5,22.6,1.9,46.5,3.4,67.1,13.2,33.7,14.5,48.2,49.8,40.6,82.2-6.6,29.3-24.7,45.9-40.5,67.8-4.4,5.8-28.3,44.8-14.5,40.9,3.9.1,26.7-20.1,21-8.6-5.8,10.1-8.8,20.9-10.1,33-4.1,23.9-3.1,52.6,7.1,74.8,1.9,3.3,3.1,3.3,3.9-.1,1.1-4.5,1.8-9.5,3.5-14.3,1.7-4.6,6.1-17.7,10.5-19.5.8,0,1.3,1.3,1.5,3.4.3,2.6,0,6.2,0,9.1,0,6.4,2.8,38.3,20.3,69.5,5.9,10.5,15.7,25.4,31.4,40.1,9.3,8.2,14.6,12.8,15,11,0-.4-.5-4-.6-5.2-.3-3-.6-5.2-.2-9.6,1-10.4,4.6-20.4,8.6-30,4.2-9.7,10.5-19.6,15.6-28.2,10.2-17.9,24-34.9,38.2-50.2,16.4-17.4,61.4-63.2,68.4-57.8.2,8.4-14.2,27.5-20.3,34.5-1.9,2.2-4.1,4.5-6.2,6.6-9.5,9.1-11.4,12.1.7,8.5,8.9-3,19.2-7.9,27.6-12.4,13-7,25.3-13.8,36.6-24.3,8.2-7.5,14.7-16.1,20.1-26,9.3-14.3,14.3-43.1,19-38,5,7,5,28.2,5.9,39.1.6,3.9-.7,17.3,4,13.7,10.1-11.1,14.2-30.5,17.1-44.9.6-1.9,1-10.7,4.2-6.4,6,12.3,7.1,26.3,8,40,1.5,13.7-3.6,43.1,4.4,26.7,6.7-14.5,14.5-29.8,17.3-44.5,2.1-9.9,2.5-19.2,2.5-28.7,0-14.3,0-25.4-3.5-44.1-3.1-15.3-7.7-28-14.4-41.8-4.4-9.2-8.3-15.4-10.9-21.8-1.5-5.2,5.8.3,7.3,1.5,5.8,4.8,10.2,9.3,15.1,15.3,2.3,2.9,4.6,6,6.8,9.1,8.8,12.9,14.6,26.3,12.4,6.2ZM520.8,289.4c6.4,0,11.5,5.2,11.5,11.5s-5.2,11.5-11.5,11.5-11.5-5.2-11.5-11.5,5.2-11.5,11.5-11.5ZM407.3,328.3c-13.6-1.5-27.7-2-41.6-2.2-20.2-.3-40.8-.6-60.7,2-12.8,2-25.6,3.4-37.7,7.8-2,.6-6,3.1-4.5.3h0c4.7-7.1,11.1-12.1,17.8-16.9,8.5-6.3,17-11.6,26.4-15.6,12.4-5.7,25.8-10.4,39.2-12.8,14.7-2.3,32-4.3,47.2-4.1,8.2.2,18.2-.3,26.1.7,1.4.5-2.7,1.5-5.2,2.2-4.4,1.2-7.8,2.1-12.4,3.4-5.9,1.7-11.9,3.5-16.5,5.3-1.9.7-4.8,1.9-6.6,2.9-1,.5-1.5.9-1.5,1.2.6.7,2.5.4,4.7.5,4.8,0,10.3.4,15,1,8.3,1,16.8,2.5,25.1,4.3,7.6,1.6,12.4,2.4,17.6,4.1,14.7,4.9,28.6,13.1,40,23.5,2.8,2.5,5.9,5.8,8.3,8.5,1,1.2,1.7,2,1.5,2.2-.4.4-3.6-1.1-4.6-1.5-9-4-19.8-6.3-29.4-8.7-15.6-3.6-32.2-6.1-48.5-8.2ZM478.3,533.9c-2.9,3.7-3.7,4.7-5,6-2.2,2.3-3.3,3.4-4.9,4-.8.3-3.3,1.2-5.9,0-1.9-.9-2.8-2.5-3.1-3-2-3.5-.3-7.3,0-7.8.4-.8.8-1.5,5.1-5,2.9-2.3,4.7-3.8,8-6.1,1.5-1.1,3-2.1,6-4,6.1-4,7.5-4.8,11-7.2,2.4-1.6,4.3-3,5.7-4-.7,1.8-1.9,4.7-3.8,7.9-1,1.8-1.2,1.8-3.9,6.2-3.2,5.1-3.4,5.7-5,7.9-.3.3-1.5,1.9-4,5ZM533.7,546.6c-2,4.8-4.2,9.4-5.9,14.3-5.5,14.9-9.6,30.1-15,45.1-1.8,5.3-3.5,11.1-7.7,14.9-8.5,8.8-21,2.8-21.1-8.9,0-2.4.5-4.8,1.4-7,5.5-11.5,11.6-21.1,19.7-31.5,6-7.2,12.6-14.5,19.2-20.9h.1c1.2-1.1,10.9-10.3,9.3-6ZM577.9,528.4c-3.2,3.6-8.1,5.2-12.6,2.9-4.4-2.1-6.1-7.7-5.4-12.2,1.1-5.5,5.9-11.9,9.5-17.3,5.5-8.2,8.8-16.1,12.4-26.4.2-.7.6-2,1-2.3h0c.5-.2.8,1.4.9,1.9,4.1,17.3,9.4,38.4-5.8,53.3ZM645.2,492.5c-.7,4-4.1,8-8.5,8.1-3,.2-6.3-1.4-8.8-3.1-7.1-6.6-5.1-16.8-6.7-25.2-1.9-10-4.6-20.3-10.9-28.1,0,0,0-.1,0,0,16,10.1,35.6,27.5,34.8,48.4ZM685.2,438.4c0,1.4,0,1.4-.2,2.1-.2,1.1-.9,2.8-1.6,3.7l-.5.6c-.9,1.2-2.1,2.2-3.4,2.8l-.8.4c-.8.4-1.6.6-2.4.8l-1.2.2c-1.1.2-2.1.2-3.2,0-1.3-.2-2.5-.6-3.6-1.2l-.5-.3c-.4-.3-.9-.5-1.3-.9-5.1-4.5-7-12.8-8.9-19-2-6.4-6.2-14.8-9.5-20.6-8.9-13.5-20.7-26-33.6-35.7,0,0,0,0,0,0,23.9,7.6,46.9,23.1,61.9,43.4,3.8,6.6,8.9,14.3,8.9,22.7s0,0,0,1Z"/>
|
||||||
|
<g class="st2">
|
||||||
|
<path class="st1" d="M489.8,346.7c.1-.3-.5-1.1-1.5-2.2-2.4-2.7-5.5-6-8.3-8.5-11.5-10.4-25.3-18.6-40-23.5-5.2-1.7-10-2.6-17.6-4.1-8.3-1.9-16.8-3.3-25.1-4.3-4.7-.6-10.2-1-15-1-2.2-.1-4.1.2-4.7-.5,0-.3.5-.7,1.5-1.2,1.8-1,4.7-2.1,6.6-2.9,4.6-1.8,10.7-3.6,16.5-5.3,4.6-1.3,8-2.2,12.4-3.4,2.5-.7,6.6-1.7,5.2-2.2-7.9-1-17.9-.4-26.1-.7-15.2-.2-32.6,1.8-47.2,4.1-13.4,2.5-26.8,7.1-39.2,12.8-9.5,4-18,9.3-26.4,15.6-6.7,4.8-13.2,9.8-17.8,16.7h0c-1.6,2.9,2.5.5,4.5-.1,12.1-4.4,24.9-5.8,37.7-7.8,20-2.6,40.5-2.3,60.7-2,13.9.3,28,.7,41.6,2.2,16.3,2.1,32.9,4.5,48.5,8.2,9.6,2.4,20.4,4.7,29.4,8.7,1.1.4,4.2,1.9,4.6,1.5Z"/>
|
||||||
|
<circle class="st1" cx="520.8" cy="300.9" r="11.5" transform="translate(-41.5 87.3) rotate(-9.2)"/>
|
||||||
|
<path class="st1" d="M614.5,371.3s0,0,0,0c12.9,9.7,24.7,22.2,33.6,35.7,3.3,5.8,7.5,14.2,9.5,20.6,1.9,6.2,3.7,14.5,8.9,19,.4.3.8.6,1.3.9l.5.3c1.1.6,2.3,1.1,3.6,1.2,1.1.1,2.1.1,3.2,0l1.2-.2c.8-.2,1.6-.4,2.4-.8l.8-.4c1.4-.7,2.5-1.6,3.4-2.8l.5-.6c.7-.9,1.4-2.6,1.6-3.7.1-.7.2-.7.2-2.1,0-1.1,0-.7,0-1,0-8.4-5-16.2-8.9-22.7-15-20.3-37.9-35.8-61.9-43.4Z"/>
|
||||||
|
<path class="st1" d="M610.5,444.2c0,0-.1,0,0,0,6.3,7.7,9,18.1,10.9,28.1,1.6,8.4-.4,18.5,6.7,25.2,2.4,1.7,5.7,3.3,8.8,3.1,4.3-.1,7.7-4.1,8.5-8.1.9-20.9-18.8-38.3-34.8-48.4Z"/>
|
||||||
|
<path class="st1" d="M582.8,473.1h0c-.4.3-.8,1.6-1,2.3-3.6,10.3-6.9,18.1-12.4,26.4-3.5,5.4-8.3,11.9-9.5,17.3-.7,4.5,1,10.1,5.4,12.2,4.5,2.3,9.4.7,12.6-2.9,15.3-15,10-36,5.8-53.3-.2-.5-.4-2.1-.9-2Z"/>
|
||||||
|
<path class="st1" d="M491.2,514.8c1.9-3.3,3.1-6.1,3.8-7.9-1.4,1-3.3,2.4-5.7,4-3.5,2.4-5,3.2-11,7.2-2.9,1.9-4.4,2.9-6,4-3.3,2.3-5.1,3.8-8,6.1-4.2,3.5-4.7,4.2-5.1,5-.3.6-2,4.3,0,7.8.3.5,1.2,2.1,3.1,3,2.6,1.2,5.2.3,5.9,0,1.6-.6,2.8-1.8,4.9-4,1.3-1.3,2.1-2.3,5-6,2.5-3.1,3.7-4.7,4-5,1.6-2.3,1.8-2.8,5-7.9,2.8-4.4,2.9-4.4,3.9-6.2Z"/>
|
||||||
|
<path class="st1" d="M524.5,552.5h-.1c-6.6,6.5-13.2,13.8-19.2,21-8,10.4-14.2,20.1-19.7,31.5-.9,2.2-1.4,4.6-1.4,7,0,11.7,12.5,17.6,21.1,8.9,4.2-3.8,5.9-9.6,7.7-14.9,5.3-15,9.4-30.2,15-45.1,1.7-4.9,3.9-9.5,5.9-14.3,1.6-4.4-8,4.9-9.3,5.8Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle class="st0" cx="695.1" cy="187.8" r="7.7"/>
|
||||||
|
<circle class="st0" cx="685.8" cy="222" r="4.5"/>
|
||||||
|
<ellipse class="st0" cx="731" cy="209.2" rx="4.2" ry="3.5"/>
|
||||||
|
<circle class="st0" cx="731" cy="243.6" r="7.2"/>
|
||||||
|
<circle class="st0" cx="745.2" cy="273.8" r="4.1"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6 KiB |
|
|
@ -1,411 +0,0 @@
|
||||||
"use client"
|
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
|
||||||
import {useTheme} from 'next-themes';
|
|
||||||
import {Button} from '@/components/ui/button';
|
|
||||||
import {Input} from '@/components/ui/input';
|
|
||||||
import {ScrollArea} from '@/components/ui/scroll-area';
|
|
||||||
import {Avatar, AvatarFallback} from '@/components/ui/avatar';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';
|
|
||||||
import {
|
|
||||||
Archive,
|
|
||||||
Ban,
|
|
||||||
Clock,
|
|
||||||
Download,
|
|
||||||
Info,
|
|
||||||
Key,
|
|
||||||
KeyRound,
|
|
||||||
MoreVertical,
|
|
||||||
Send,
|
|
||||||
ShieldCheck,
|
|
||||||
UserCheck,
|
|
||||||
UserX
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {usePathname} from "next/navigation";
|
|
||||||
import {useUser} from "@/contexts/user";
|
|
||||||
import {useToast} from "@/hooks/use-toast";
|
|
||||||
import {useSharedState} from "@/hooks/shared-states";
|
|
||||||
import {createBrowserClient} from '@/lib/supabase/browser'
|
|
||||||
import {CryptoManager} from "@/lib/crypto/keys";
|
|
||||||
import {REALTIME_SUBSCRIBE_STATES} from "@supabase/realtime-js";
|
|
||||||
import ChatSkeleton from "@/app/[id]/skeleton";
|
|
||||||
|
|
||||||
export default function ChatPage() {
|
|
||||||
const {toast} = useToast();
|
|
||||||
const supabase = createBrowserClient();
|
|
||||||
|
|
||||||
const [messages, setMessages] = useState<SiPher.Thread["messages"]>([]);
|
|
||||||
const [inputMessage, setInputMessage] = useState('');
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
||||||
const [showKeyDialog, setShowKeyDialog] = useState(false);
|
|
||||||
const [showUserDialog, setShowUserDialog] = useState(false);
|
|
||||||
const [isEncrypted, setIsEncrypted] = useState(true);
|
|
||||||
|
|
||||||
const [realtimeSubscribed, setRealtimeSubscribed] = useState<REALTIME_SUBSCRIBE_STATES>(REALTIME_SUBSCRIBE_STATES.CLOSED);
|
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [user, setUser] = useState<SiPher.User | null>(null);
|
|
||||||
const pathName = usePathname();
|
|
||||||
const threadId = pathName.replace("/", "");
|
|
||||||
|
|
||||||
const {
|
|
||||||
user: currentUser,
|
|
||||||
getUser
|
|
||||||
} = useUser()
|
|
||||||
|
|
||||||
const {threads} = useSharedState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const channel = supabase
|
|
||||||
.channel(`messages:${threadId}`)
|
|
||||||
.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: '*',
|
|
||||||
schema: 'public',
|
|
||||||
table: 'messages',
|
|
||||||
},
|
|
||||||
async (payload) => {
|
|
||||||
if (payload.eventType === "INSERT") {
|
|
||||||
try {
|
|
||||||
const messageData = payload.new as SiPher.RealtimeMessageData;
|
|
||||||
const isSender = messageData.sender_uuid === currentUser.uuid;
|
|
||||||
|
|
||||||
const decryptedMsg = await CryptoManager.decryptMessage(
|
|
||||||
// I forgot to add this, without this, it's pretty much unusable.
|
|
||||||
isSender ? messageData.sender_content : messageData.recipient_content
|
|
||||||
)
|
|
||||||
|
|
||||||
setMessages((prevState) => {
|
|
||||||
return [
|
|
||||||
...prevState,
|
|
||||||
{
|
|
||||||
id: messageData.id,
|
|
||||||
content: decryptedMsg,
|
|
||||||
sender_uuid: messageData.sender_uuid,
|
|
||||||
created_at: messageData.created_at,
|
|
||||||
isSender
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`Something went wrong on the message update: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.subscribe((status) => {
|
|
||||||
setRealtimeSubscribed(status)
|
|
||||||
console.info(`Subscription for thread ${threadId} has the status "${status}"`)
|
|
||||||
console.info("If closed, something bad might be happening at the backend.")
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
supabase.removeChannel(channel)
|
|
||||||
}
|
|
||||||
}, [threadId, currentUser.uuid, supabase])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getUserDataAndChat = async () => {
|
|
||||||
const {thread: getThread} = await (await fetch(`/api/user/get/thread?threadId=${threadId}`)).json() as {
|
|
||||||
thread: SiPher.Thread
|
|
||||||
};
|
|
||||||
|
|
||||||
const otherUser = getThread.participant_suuids.filter((ids) => ids !== currentUser.suuid);
|
|
||||||
const user = await getUser(`Being called from chat page (${threadId}`, otherUser[0], "suuid", true)
|
|
||||||
|
|
||||||
if (!(user.user[0].suuid && user.user[0].username)) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Could not verify the existence of this user",
|
|
||||||
variant: "destructive",
|
|
||||||
duration: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser(user.user[0])
|
|
||||||
|
|
||||||
const decryptedMsg = await CryptoManager.decryptThreadMessages(getThread["messages"], currentUser.uuid)
|
|
||||||
setMessages(decryptedMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threads.length > 0) {
|
|
||||||
setIsLoaded(true)
|
|
||||||
getUserDataAndChat()
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setUser(null)
|
|
||||||
setMessages([])
|
|
||||||
setIsLoaded(false)
|
|
||||||
}
|
|
||||||
}, [threadId, currentUser.uuid, supabase]) // Never trusting the lint again
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!realtimeSubscribed) return;
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
if (realtimeSubscribed === 'TIMED_OUT' || realtimeSubscribed === 'CLOSED') {
|
|
||||||
toast({
|
|
||||||
title: "Connection Issue",
|
|
||||||
description: "You might need to restart your browser due to connection issues.",
|
|
||||||
variant: "destructive",
|
|
||||||
duration: 10000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [realtimeSubscribed, toast]);
|
|
||||||
|
|
||||||
if (!isLoaded || !user || realtimeSubscribed !== "SUBSCRIBED") {
|
|
||||||
return <ChatSkeleton/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkUserValidity = async () => {
|
|
||||||
// Implementation for checking user validity
|
|
||||||
setShowUserDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkCurrentKey = async () => {
|
|
||||||
// Implementation for checking current key
|
|
||||||
setShowKeyDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUser = async () => {
|
|
||||||
// Implementation for deleting user
|
|
||||||
setShowDeleteDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendMessage = async (content: string) => {
|
|
||||||
if (!content.trim()) return;
|
|
||||||
setInputMessage('');
|
|
||||||
|
|
||||||
await CryptoManager.prepareAndSendMessage(
|
|
||||||
content,
|
|
||||||
currentUser.public_key,
|
|
||||||
user.public_key,
|
|
||||||
threadId
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-screen max-h-[900px] w-full">
|
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Avatar>
|
|
||||||
<AvatarFallback>
|
|
||||||
{
|
|
||||||
user.username.charAt(0).toLocaleUpperCase()
|
|
||||||
}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold">
|
|
||||||
{
|
|
||||||
user.username.charAt(0).toLocaleUpperCase() + user.username.slice(1)
|
|
||||||
}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="text-primary">
|
|
||||||
{isEncrypted ? <ShieldCheck className="h-5 w-5"/> : <Ban className="h-5 w-5"/>}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{isEncrypted ? 'Encrypted Chat' : 'Encryption Issue'}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreVertical className="h-5 w-5"/>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
|
||||||
<DropdownMenuLabel>Chat Options</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator/>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={checkUserValidity}>
|
|
||||||
<UserCheck className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Check User</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={checkCurrentKey}>
|
|
||||||
<Key className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Check Current Key</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator/>
|
|
||||||
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Clock className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Message History</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Archive className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Archive Chat</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Download className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Export Chat</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator/>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={deleteUser} className="text-red-500">
|
|
||||||
<UserX className="mr-2 h-4 w-4"/>
|
|
||||||
<span>Delete User</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="flex-1 p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<AnimatePresence>
|
|
||||||
{messages.map((message) => (
|
|
||||||
<motion.div
|
|
||||||
key={message.id}
|
|
||||||
initial={{opacity: 0, y: 20}}
|
|
||||||
animate={{opacity: 1, y: 0}}
|
|
||||||
exit={{opacity: 0}}
|
|
||||||
className={`flex ${message.isSender ? 'justify-end' : 'justify-start'}`}
|
|
||||||
>
|
|
||||||
<div className={`max-w-[70%] rounded-lg p-3 ${
|
|
||||||
message.isSender
|
|
||||||
? message.error ? 'bg-red-500' : 'bg-primary text-primary-foreground'
|
|
||||||
: message.error ? 'bg-red-500' : 'bg-secondary'
|
|
||||||
}`}>
|
|
||||||
<p>{message.content}</p>
|
|
||||||
<div className="flex items-center justify-end space-x-1 mt-1">
|
|
||||||
<span className="text-xs opacity-70">
|
|
||||||
{new Date(message.created_at).toLocaleTimeString([], {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<div className="p-4 border-t">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Input
|
|
||||||
value={inputMessage}
|
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
|
||||||
placeholder="Type a message..."
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessage(inputMessage);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button onClick={() => sendMessage(inputMessage)}>
|
|
||||||
<Send className="h-4 w-4"/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Delete User</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
Are you sure you want to delete this user? This will remove them from your contacts
|
|
||||||
and delete all messages. This action cannot be undone.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction className="bg-red-500">Delete</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
|
|
||||||
<AlertDialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Encryption Status</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<KeyRound className="h-4 w-4 text-green-500"/>
|
|
||||||
<span>Local private key is valid and active</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Key className="h-4 w-4 text-green-500"/>
|
|
||||||
<span>Remote public key is verified</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<ShieldCheck className="h-4 w-4 text-green-500"/>
|
|
||||||
<span>End-to-end encryption is active</span>
|
|
||||||
</div>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction>Close</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
|
|
||||||
<AlertDialog open={showUserDialog} onOpenChange={setShowUserDialog}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>User Verification</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<UserCheck className="h-4 w-4 text-green-500"/>
|
|
||||||
<span>User is verified and active</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Info className="h-4 w-4"/>
|
|
||||||
<span>Last active: 2 minutes ago</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<ShieldCheck className="h-4 w-4 text-green-500"/>
|
|
||||||
<span>Secure connection established</span>
|
|
||||||
</div>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction>Close</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import {Skeleton} from "@/components/ui/skeleton";
|
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area";
|
|
||||||
|
|
||||||
export default function ChatSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-screen max-h-[900px] w-full animate-in fade-in-50">
|
|
||||||
{/* Header Skeleton */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Skeleton className="h-10 w-10 rounded-full"/>
|
|
||||||
<Skeleton className="h-4 w-32"/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Skeleton className="h-9 w-9 rounded-md"/>
|
|
||||||
<Skeleton className="h-9 w-9 rounded-md"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages Skeleton */}
|
|
||||||
<ScrollArea className="flex-1 p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Left message */}
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<Skeleton className="h-16 w-[250px] rounded-lg"/>
|
|
||||||
</div>
|
|
||||||
{/* Right message */}
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Skeleton className="h-12 w-[200px] rounded-lg"/>
|
|
||||||
</div>
|
|
||||||
{/* Left message */}
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<Skeleton className="h-20 w-[300px] rounded-lg"/>
|
|
||||||
</div>
|
|
||||||
{/* Right message */}
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Skeleton className="h-14 w-[180px] rounded-lg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{/* Input Area Skeleton */}
|
|
||||||
<div className="p-4 border-t">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Skeleton className="h-10 flex-1 rounded-md"/>
|
|
||||||
<Skeleton className="h-10 w-10 rounded-md"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
"use client"
|
|
||||||
import {motion} from "framer-motion";
|
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
|
||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";
|
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import {AlertTriangle, KeyRound, Lock, MessageSquare, Shield, UserCheck,} from "lucide-react";
|
|
||||||
|
|
||||||
export default function AboutPage() {
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: {opacity: 0},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: {opacity: 0, y: 20},
|
|
||||||
visible: {opacity: 1, y: 0}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="container max-w-4xl mx-auto py-8 px-4 space-y-8"
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
variants={containerVariants}
|
|
||||||
>
|
|
||||||
<motion.div variants={itemVariants} className="text-center space-y-4">
|
|
||||||
<h1 className="text-4xl font-bold">About SiPher</h1>
|
|
||||||
<p className="text-lg text-muted-foreground">
|
|
||||||
Where privacy meets simplicity in secure communication
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Separator/>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTriangle className="h-4 w-4"/>
|
|
||||||
<AlertTitle>Important Notice</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
SiPher is a CS50X final project and is not intended for production use.
|
|
||||||
While we implement strong encryption, please do not use it for sensitive communications.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>How SiPher Works</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Understanding the security behind your messages
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<KeyRound className="h-6 w-6 text-primary mt-1"/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">Key Generation</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Each user has a unique public-private key pair generated in their browser. Lost it and didn't
|
|
||||||
make a
|
|
||||||
backup? Welp, skill issue I guess.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<Lock className="h-6 w-6 text-primary mt-1"/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">End-to-End Encryption</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Messages are encrypted before leaving your device
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<Shield className="h-6 w-6 text-primary mt-1"/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">Zero (And A Half) Trust</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Server never sees your decrypted messages. But we do store their encrypted version though lmao.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<UserCheck className="h-6 w-6 text-primary mt-1"/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">User Privacy</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Users are identified by unique IDs, not personal information. No e-mail, no nothing, only your ID
|
|
||||||
(and probably IP due to Supabase logging it)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Technical Details</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
The technology powering SiPher's "security"
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-semibold">Encryption</h3>
|
|
||||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
|
||||||
<li>RSA-OAEP for key exchange</li>
|
|
||||||
<li>AES-GCM for message encryption</li>
|
|
||||||
<li>PBKDF2 for key derivation</li>
|
|
||||||
<li>SHA-256 for message integrity</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-semibold">Implementation</h3>
|
|
||||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
|
||||||
<li>Web Crypto API for cryptographic operations</li>
|
|
||||||
<li>Next.js for the application framework</li>
|
|
||||||
<li>Supabase for real-time messaging</li>
|
|
||||||
<li>TailwindCSS and ShadcnUI for the interface (I suck at design)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Frequently Asked Questions</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="item-1">
|
|
||||||
<AccordionTrigger>How secure are my messages?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
Messages are encrypted using industry-standard algorithms and never stored in plaintext.
|
|
||||||
However, as this is an educational project, I recommend not using it for sensitive communications.
|
|
||||||
If you do and I get a notice, I will give out the data I have on you. I don't care.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-2">
|
|
||||||
<AccordionTrigger>What happens if I lose my private key?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
If you lose your private key, you won't be able to decrypt previous messages.
|
|
||||||
You can generate a new key pair, but you'll need to start fresh conversations, previous messages
|
|
||||||
from
|
|
||||||
other conversations will be lost forever.
|
|
||||||
Always backup your private key in the settings.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-3">
|
|
||||||
<AccordionTrigger>Can I recover deleted messages?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
You can't even delete chats, imagine messages lmao.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-4">
|
|
||||||
<AccordionTrigger>How do I verify a user's identity?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
Each user has a unique SUUID (Short UUID) that can be shared and verified.
|
|
||||||
You can verify a user's identity by comparing their SUUID in a secure channel.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-5">
|
|
||||||
<AccordionTrigger>Is SiPher open source?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
Not yet. As this is a CS50X final project, the code will be made available
|
|
||||||
for educational purposes in the future.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="item-5">
|
|
||||||
<AccordionTrigger>Will you continue this project after submitting it?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
Probably. It's quite fun dealing with encryption.
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Message Flow</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
How your message travels from you to the other user
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="relative flex justify-between items-center py-8">
|
|
||||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
|
||||||
<MessageSquare className="w-6 h-6 text-primary"/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute left-[calc(50%-4px)] top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-primary"/>
|
|
||||||
<div className="absolute left-[20%] right-[20%] top-1/2 -translate-y-1/2 h-0.5 bg-primary/20"/>
|
|
||||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
|
||||||
<Lock className="w-6 h-6 text-primary"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-center text-muted-foreground">
|
|
||||||
Messages are encrypted on your device before being sent through our servers,
|
|
||||||
ensuring end-to-end encryption for all communications.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants} className="text-center text-sm text-muted-foreground">
|
|
||||||
<p>Built with 💖 as a CS50X final project</p>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
3
src/app/api/auth/[...all]/route.ts
Normal file
3
src/app/api/auth/[...all]/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { nextJsHandler } from "@convex-dev/better-auth/nextjs";
|
||||||
|
|
||||||
|
export const { GET, POST } = nextJsHandler();
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
|
||||||
|
|
||||||
// Helper function to get user data by UUID
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
try {
|
|
||||||
const supabase = await createClient();
|
|
||||||
const {searchParams} = new URL(request.url);
|
|
||||||
const uuid = searchParams.get('uuid');
|
|
||||||
const suuid = searchParams.get('suuid');
|
|
||||||
const getDetails = searchParams.get("detailed")
|
|
||||||
|
|
||||||
if (uuid) {
|
|
||||||
// Get specific user by UUID
|
|
||||||
const userData = await getUserByUUID(supabase, uuid);
|
|
||||||
return NextResponse.json({user: userData});
|
|
||||||
} else if (suuid) {
|
|
||||||
const {data, error} = await supabase.rpc('search_users', {
|
|
||||||
search_term: suuid
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({error: error}, {status: 500});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getDetails) {
|
|
||||||
return NextResponse.json({user: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
|
||||||
} else {
|
|
||||||
// Get current authenticated user
|
|
||||||
const {data: {user}, error: authError} = await supabase.auth.getUser();
|
|
||||||
if (authError) throw authError;
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({user: null}, {status: 401});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await getUserByUUID(supabase, user.id);
|
|
||||||
return NextResponse.json({user: userData});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof error === "object") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: `Failed to fetch user: ${JSON.stringify(error)}`},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: `Failed to fetch user: ${error}`},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// app/api/auth/login/route.ts
|
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const {username, password} = await request.json()
|
|
||||||
const supabase = await createClient()
|
|
||||||
|
|
||||||
const domain = process.env.DOMAIN;
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Server is misconfigured, please check env variables and try again."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mocks the email with the domain we configured on the local env
|
|
||||||
const email = `${username.toLowerCase()}@${domain}`
|
|
||||||
|
|
||||||
// Sends the request through supabase
|
|
||||||
const {data: {user}, error: authError} = await supabase.auth.signInWithPassword({
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (authError) throw authError
|
|
||||||
|
|
||||||
// Fetch our custom user data
|
|
||||||
const {data: userData, error: userError} = await supabase
|
|
||||||
.from('users')
|
|
||||||
.select('*, public_key')
|
|
||||||
.eq('uuid', user?.id)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (userError) throw userError
|
|
||||||
|
|
||||||
// Returns simple data
|
|
||||||
return NextResponse.json({user: userData})
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: `Login failed: ${error}`},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import {NextResponse} from 'next/server'
|
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const {username, password, public_key} = await request.json()
|
|
||||||
const supabase = await createClient()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const domain = process.env.DOMAIN;
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Server is misconfigured, please check env variables and try again."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500
|
|
||||||
})
|
|
||||||
} else if (!username || !password || !public_key) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Missing params"
|
|
||||||
}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
// First create the auth user
|
|
||||||
const {data: {user}, error: authError} = await supabase.auth.signUp({
|
|
||||||
email: `${username}@${domain}`, // Using username as email
|
|
||||||
password: password,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (authError) throw authError
|
|
||||||
if (!user) throw new Error('No user returned from sign up')
|
|
||||||
|
|
||||||
// Then create our custom user record
|
|
||||||
const {error: insertError} = await supabase
|
|
||||||
.from('users')
|
|
||||||
.insert({
|
|
||||||
uuid: user.id,
|
|
||||||
username: username,
|
|
||||||
public_key
|
|
||||||
})
|
|
||||||
|
|
||||||
if (insertError) {
|
|
||||||
// Rollback auth user if custom user creation fails
|
|
||||||
await supabase.auth.admin.deleteUser(user.id)
|
|
||||||
throw insertError
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({success: true})
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof error === "object") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: JSON.stringify(error)},
|
|
||||||
{status: 400}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: `Registration failed: ${error}`},
|
|
||||||
{status: 400}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
const {participant} = await req.json();
|
|
||||||
|
|
||||||
if (!participant) {
|
|
||||||
return NextResponse.json({error: 'Participant not found'}, {status: 400});
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = await createClient()
|
|
||||||
|
|
||||||
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
|
||||||
console.log("From user: ", user?.id)
|
|
||||||
if (userError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: userError},
|
|
||||||
{status: userError?.status}
|
|
||||||
)
|
|
||||||
} else if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "User not found"},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** First we need to check if the requested participant is in the user's request array */
|
|
||||||
const dbUser = await getUserByUUID(supabase, user.id)
|
|
||||||
|
|
||||||
if (!dbUser) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "User not found"},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requests = dbUser.requests as string[]
|
|
||||||
|
|
||||||
if (!requests.includes(participant)) {
|
|
||||||
return NextResponse.json({error: "Requested user not in requests array."}, {status: 400})
|
|
||||||
} else if (participant === dbUser.suuid) {
|
|
||||||
return NextResponse.json({error: "Cannot add self to a new thread"}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Then we can create the thread */
|
|
||||||
|
|
||||||
const {error} = await supabase.rpc('create_private_thread', {
|
|
||||||
participant_suuid: participant
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({error}, {status: 500});
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({success: true}, {status: 200});
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
try {
|
|
||||||
const {searchParams} = new URL(request.url);
|
|
||||||
const threadId = searchParams.get('threadId');
|
|
||||||
|
|
||||||
if (!threadId) {
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "No thread id provided"
|
|
||||||
}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
|
||||||
|
|
||||||
if (userError) {
|
|
||||||
NextResponse.json(
|
|
||||||
{error: userError},
|
|
||||||
{status: userError?.status}
|
|
||||||
)
|
|
||||||
} else if (!user) {
|
|
||||||
NextResponse.json(
|
|
||||||
{error: "User not found"},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data, error} = await supabase.rpc(
|
|
||||||
"get_thread",
|
|
||||||
{
|
|
||||||
thread_uuid: threadId,
|
|
||||||
user_id: user!.id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({error}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({thread: data[0]}, {status: 200});
|
|
||||||
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log(e)
|
|
||||||
if (typeof e === "object") {
|
|
||||||
return NextResponse.json({error: JSON.stringify(e)}, {status: 500})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({error: e}, {status: 500})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
|
||||||
|
|
||||||
if (userError) {
|
|
||||||
NextResponse.json(
|
|
||||||
{error: userError},
|
|
||||||
{status: userError?.status}
|
|
||||||
)
|
|
||||||
} else if (!user) {
|
|
||||||
NextResponse.json(
|
|
||||||
{error: "User not found"},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data, error} = await supabase.rpc(
|
|
||||||
"get_user_threads",
|
|
||||||
{
|
|
||||||
user_id: user!.id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({error}, {status: 400})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({threads: data}, {status: 200});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
try {
|
|
||||||
const supabase = await createClient();
|
|
||||||
const {searchParams} = new URL(request.url);
|
|
||||||
const uuid = searchParams.get('uuid');
|
|
||||||
const getDetails = searchParams.get("detailed")
|
|
||||||
|
|
||||||
if (!uuid) {
|
|
||||||
return NextResponse.json({error: "Missing UUID from request"}, {status: 400})
|
|
||||||
} else if (uuid.length > 10) {
|
|
||||||
return NextResponse.json({error: "UUID is not valid."}, {status: 400});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
|
||||||
|
|
||||||
if (userError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: userError},
|
|
||||||
{status: userError?.status}
|
|
||||||
)
|
|
||||||
} else if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "User not found"},
|
|
||||||
{status: 401}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data, error} = await supabase.rpc('search_users', {
|
|
||||||
search_term: uuid
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({error: error}, {status: 500});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getDetails) {
|
|
||||||
return NextResponse.json({user: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json({error: error}, {status: 500});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const {threadId, senderContent, recipientContent} = await request.json();
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const {data, error} = await supabase.rpc('send_message', {
|
|
||||||
thread_uuid: threadId,
|
|
||||||
sender_content: senderContent,
|
|
||||||
recipient_content: recipientContent
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
return NextResponse.json({messageId: data});
|
|
||||||
} catch (error: any) {
|
|
||||||
if (typeof error === "object") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: 'Failed to send message', details: error.message},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
|
||||||
import updateUserRequests from "@/lib/api/helpers/updateUserRequests";
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const {searchTerm} = await request.json();
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Missing required fields"},
|
|
||||||
{status: 400}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data: {user}, error: authError} = await supabase.auth.getUser();
|
|
||||||
if (authError) throw authError;
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({user: null}, {status: 401});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUser = await getUserByUUID(supabase, user.id)
|
|
||||||
const userSuuid = getUser.suuid;
|
|
||||||
|
|
||||||
if (userSuuid === searchTerm) {
|
|
||||||
return NextResponse.json({success: false, hint: "Cannot send request to self"}, {status: 409});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateUserRequests(searchTerm, userSuuid, supabase);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: result.error},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({success: true});
|
|
||||||
} catch (err) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: `Failed to update requests: ${err}`},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const {publicKey} = await request.json();
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const {error} = await supabase
|
|
||||||
.from('users')
|
|
||||||
.update({public_key: publicKey})
|
|
||||||
.eq('uuid', (await supabase.auth.getUser()).data.user?.id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
return NextResponse.json({success: true});
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: 'Failed to update public key'},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
src/app/auth/components/sign-in-form.tsx
Normal file
87
src/app/auth/components/sign-in-form.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
|
import { ErrorContext } from "better-auth/react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function SignInForm(
|
||||||
|
{ captchaToken }: { captchaToken: string | null }
|
||||||
|
) {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSignIn = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await authClient.signIn.username(
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
fetchOptions: {
|
||||||
|
headers: {
|
||||||
|
"x-captcha-response": captchaToken ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: () => {
|
||||||
|
setLoading(true);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setLoading(false);
|
||||||
|
toast.success("Signed in successfully");
|
||||||
|
redirect("/");
|
||||||
|
},
|
||||||
|
onError: (ctx: ErrorContext) => {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(ctx.error.message);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSignIn} className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="john_doe"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="bg-background/50 focus:bg-background transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="********"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="bg-background/50 focus:bg-background transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full font-semibold mt-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
234
src/app/auth/components/sign-up-form.tsx
Normal file
234
src/app/auth/components/sign-up-form.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
|
import { ErrorContext } from "better-auth/react";
|
||||||
|
import { Check, Eye, EyeOff, Loader2, RefreshCw, X } from "lucide-react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function SignUpForm(
|
||||||
|
{ captchaToken }: { captchaToken: string | null }
|
||||||
|
) {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [isUsernameAvailable, setIsUsernameAvailable] = useState<boolean | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isValidatingUsername, setIsValidatingUsername] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const handleSignUp = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
toast.error("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length > 30) {
|
||||||
|
toast.error("Password must be less than 30 characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await authClient.signUp.email(
|
||||||
|
{
|
||||||
|
email: `${username}.user@sipher.space`,
|
||||||
|
name: username,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
fetchOptions: {
|
||||||
|
headers: {
|
||||||
|
"x-captcha-response": captchaToken ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: () => {
|
||||||
|
setLoading(true);
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setLoading(false);
|
||||||
|
toast.success("Account created successfully, logging in...");
|
||||||
|
await authClient.signIn.username(
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
fetchOptions: {
|
||||||
|
headers: {
|
||||||
|
"x-captcha-response": captchaToken ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Logged in successfully");
|
||||||
|
redirect("/");
|
||||||
|
},
|
||||||
|
onError: (ctx: ErrorContext) => {
|
||||||
|
toast.error(ctx.error.message);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (ctx: ErrorContext) => {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(ctx.error.message);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||||
|
let newPassword = "";
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
newPassword += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
setPassword(newPassword);
|
||||||
|
setConfirmPassword(newPassword);
|
||||||
|
navigator.clipboard.writeText(newPassword);
|
||||||
|
toast.success("Password generated and copied to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSignUp} className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="john_doe"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setUsername(val);
|
||||||
|
if (val) {
|
||||||
|
setIsValidatingUsername(true);
|
||||||
|
// @ts-ignore
|
||||||
|
const isValid = await authClient.isUsernameAvailable({ username: val });
|
||||||
|
setIsUsernameAvailable(!!isValid);
|
||||||
|
setIsValidatingUsername(false);
|
||||||
|
} else {
|
||||||
|
setIsUsernameAvailable(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`bg-background/50 focus:bg-background transition-colors pr-10 ${isUsernameAvailable === false ? "border-red-500 focus-visible:ring-red-500" :
|
||||||
|
isUsernameAvailable === true ? "border-green-500 focus-visible:ring-green-500" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
|
{isValidatingUsername ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : isUsernameAvailable === true ? (
|
||||||
|
<Check className="size-4 text-green-500" />
|
||||||
|
) : isUsernameAvailable === false ? (
|
||||||
|
<X className="size-4 text-red-500" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isUsernameAvailable === false && (
|
||||||
|
<p className="text-xs text-red-500">Username is already taken</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="********"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className={`bg-background/50 focus:bg-background transition-colors pr-24 ${password.length >= 8 && password.length <= 30
|
||||||
|
? "border-green-500 focus-visible:ring-green-500"
|
||||||
|
: password.length > 30
|
||||||
|
? "border-red-500 focus-visible:ring-red-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||||
|
{password.length > 30 ? (
|
||||||
|
<X className="size-4 text-red-500" />
|
||||||
|
) : password.length >= 8 && (
|
||||||
|
<Check className="size-4 text-green-500" />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-6 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
title={showPassword ? "Hide password" : "Show password"}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-6 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={generatePassword}
|
||||||
|
title="Generate secure password"
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{password.length > 30 && (
|
||||||
|
<p className="text-xs text-red-500">Password must be less than 30 characters</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="********"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className={`bg-background/50 focus:bg-background transition-colors pr-10 ${confirmPassword && password === confirmPassword && password.length <= 30
|
||||||
|
? "border-green-500 focus-visible:ring-green-500"
|
||||||
|
: (confirmPassword && password !== confirmPassword) || (confirmPassword && password.length > 30)
|
||||||
|
? "border-red-500 focus-visible:ring-red-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
|
{confirmPassword && password === confirmPassword && password.length <= 30 ? (
|
||||||
|
<Check className="size-4 text-green-500" />
|
||||||
|
) : confirmPassword && (password !== confirmPassword || password.length > 30) ? (
|
||||||
|
<X className="size-4 text-red-500" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{confirmPassword && password !== confirmPassword && (
|
||||||
|
<p className="text-xs text-red-500">Passwords do not match</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full font-semibold mt-2"
|
||||||
|
disabled={
|
||||||
|
loading ||
|
||||||
|
isUsernameAvailable === false ||
|
||||||
|
password !== confirmPassword ||
|
||||||
|
password.length < 8 ||
|
||||||
|
password.length > 30
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : "Sign Up"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param username - The unique username of that user. This will be checked for collision.
|
|
||||||
* @param password - The plain-text password of the user. Supabase will try to match it.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export default async function Login(username: string, password: string) {
|
|
||||||
try {
|
|
||||||
let response = await fetch('/api/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({username, password}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simple error handling.
|
|
||||||
// Since we mock an email on the main app to bypass Supabase's authentication method, we can just return whatever the API returns.
|
|
||||||
// This also means this might be insecure, but oh well. Don't lose your password, I guess?
|
|
||||||
let resData = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return ({
|
|
||||||
code: resData.code,
|
|
||||||
message: resData.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ({
|
|
||||||
code: 200,
|
|
||||||
message: resData.data
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return {code: 500, message: "An unknown error occurred"};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useState} from 'react'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import {motion} from 'framer-motion'
|
|
||||||
import {Button} from "@/components/ui/button"
|
|
||||||
import {Input} from "@/components/ui/input"
|
|
||||||
import {Label} from "@/components/ui/label"
|
|
||||||
import {Card, CardContent} from "@/components/ui/card"
|
|
||||||
import {EyeIcon, EyeOffIcon} from 'lucide-react'
|
|
||||||
import {useToast} from "@/hooks/use-toast"
|
|
||||||
import {ToastActionElement} from "@/components/ui/toast";
|
|
||||||
import {useUser} from "@/contexts/user";
|
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import {useTheme} from "next-themes";
|
|
||||||
import Register from "@/app/auth/login/register";
|
|
||||||
import Login from "@/app/auth/login/login";
|
|
||||||
|
|
||||||
export default function AuthPage() {
|
|
||||||
const {checkAuth} = useUser();
|
|
||||||
const {theme, systemTheme} = useTheme()
|
|
||||||
const {toast} = useToast();
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [isLogin, setIsLogin] = useState(true);
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const check = useCallback(async () => {
|
|
||||||
const isAuthenticated = await checkAuth("Called on Login page");
|
|
||||||
if (isAuthenticated) {
|
|
||||||
router.replace('/');
|
|
||||||
} else {
|
|
||||||
setMounted(true);
|
|
||||||
}
|
|
||||||
}, [checkAuth, router, setMounted])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
check().then(() => {
|
|
||||||
console.log("Login page check finished")
|
|
||||||
})
|
|
||||||
}, [check]);
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return <div className="min-h-screen flex items-center justify-center">
|
|
||||||
<svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
|
||||||
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"/>
|
|
||||||
</svg>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const getTheme = () => {
|
|
||||||
if (theme === "system") {
|
|
||||||
switch (systemTheme) {
|
|
||||||
case "dark":
|
|
||||||
return "dark"
|
|
||||||
default:
|
|
||||||
return "light"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return theme === "dark" ? "dark" : "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoSrc = getTheme() === 'dark' ? '/logos/logo-light.png' : '/logos/logo.png';
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
const username = (document.getElementById('username') as HTMLInputElement).value;
|
|
||||||
const password = (document.getElementById('password') as HTMLInputElement).value;
|
|
||||||
|
|
||||||
let response: {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
action?: ToastActionElement | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLogin) {
|
|
||||||
response = await Register(username, password);
|
|
||||||
} else {
|
|
||||||
response = await Login(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code !== 200) {
|
|
||||||
const msg = response.message
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(msg);
|
|
||||||
let desc = parsed.name;
|
|
||||||
|
|
||||||
switch (desc) {
|
|
||||||
case "AuthWeakPasswordError": {
|
|
||||||
desc = "Password too weak, please try again.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
desc = "An unknown error occurred";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: desc,
|
|
||||||
variant: "destructive",
|
|
||||||
duration: 5000
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If msg isn't valid JSON, show the raw message
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: msg,
|
|
||||||
variant: "destructive",
|
|
||||||
duration: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: response.message,
|
|
||||||
variant: "default",
|
|
||||||
duration: 5000, // Increased duration for better visibility
|
|
||||||
});
|
|
||||||
window.location.href = "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}, 2000)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/20 to-secondary/20 p-4">
|
|
||||||
<Card className="w-full max-w-4xl overflow-hidden">
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="flex flex-col md:flex-row min-h-[480px]">
|
|
||||||
<div
|
|
||||||
className="md:w-1/2 bg-primary p-8 text-primary-foreground flex flex-col justify-center items-center">
|
|
||||||
<Image
|
|
||||||
src={logoSrc}
|
|
||||||
alt="SiPher"
|
|
||||||
width={120}
|
|
||||||
height={120}
|
|
||||||
className="mb-8"
|
|
||||||
/>
|
|
||||||
<h1 className="text-3xl font-bold mb-4 text-center">
|
|
||||||
Silent Whisper
|
|
||||||
</h1>
|
|
||||||
<p className="text-center mb-8">
|
|
||||||
Trust the shadows. Whisper safely.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="md:w-1/2 p-8">
|
|
||||||
<motion.div
|
|
||||||
initial={{opacity: 0, y: 20}}
|
|
||||||
animate={{opacity: 1, y: 0}}
|
|
||||||
transition={{duration: 0.5}}
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl font-semibold mb-6 text-center">
|
|
||||||
{isLogin ? "Sign In" : "Sign Up"}
|
|
||||||
</h2>
|
|
||||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="username">
|
|
||||||
Username
|
|
||||||
</Label>
|
|
||||||
<Input id="username" type="text" placeholder="johndoe"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="password">
|
|
||||||
Password
|
|
||||||
</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
className="pr-10"
|
|
||||||
placeholder="********"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOffIcon className="h-5 w-5 text-gray-400"/>
|
|
||||||
) : (
|
|
||||||
<EyeIcon className="h-5 w-5 text-gray-400"/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "One second, please..." : (isLogin ? "Sign In" : "Sign Up")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => setIsLogin(!isLogin)}
|
|
||||||
className="text-sm"
|
|
||||||
>
|
|
||||||
{isLogin
|
|
||||||
? "Don't have an account? Sign Up"
|
|
||||||
: "Already have an account? Sign In"
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import {CryptoManager} from "@/lib/crypto/keys";
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param username - The unique username of that user. This will be checked for collision.
|
|
||||||
* @param password - The plain-text password of the user. Will be encrypted later by Supabase
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export default async function Register(username: string, password: string) {
|
|
||||||
try {
|
|
||||||
const keyPair = await CryptoManager.generateUserKeys();
|
|
||||||
await CryptoManager.storePrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
// Export public key for server
|
|
||||||
const exportedPublic = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
|
||||||
|
|
||||||
// Sends the request to the API
|
|
||||||
let res = await fetch('/api/auth/register', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({username, password, public_key: exportedPublic}), // Stringifies the JSON
|
|
||||||
});
|
|
||||||
|
|
||||||
// Default error handler, if not OK just return whatever the API returned
|
|
||||||
if (!res.ok) {
|
|
||||||
let data = await res.json();
|
|
||||||
return {
|
|
||||||
code: res.status,
|
|
||||||
message: data.error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User was created, now it just needs to login on the service.
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: "User created successfully, go ahead and login."
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return {
|
|
||||||
code: 500,
|
|
||||||
message: e.error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
158
src/app/auth/page.tsx
Normal file
158
src/app/auth/page.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Captcha, { CaptchaRef } from "@/components/ui/captcha";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { SignInForm } from "./components/sign-in-form";
|
||||||
|
import { SignUpForm } from "./components/sign-up-form";
|
||||||
|
|
||||||
|
export default function AuthPage() {
|
||||||
|
const { data, error, isPending } = authClient.useSession();
|
||||||
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||||
|
const [method, setMethod] = useState<"signIn" | "signUp">("signIn");
|
||||||
|
const captchaRef = useRef<CaptchaRef>(null);
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen w-full bg-background">
|
||||||
|
<Spinner className="size-10 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && error.status !== 404) {
|
||||||
|
console.error("[AuthPage] > Error:", error);
|
||||||
|
toast.error(error.message);
|
||||||
|
} else if (data) {
|
||||||
|
console.log(`[AuthPage] > User ${data.user.username} logged in, redirecting to home...`);
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMethod = () => {
|
||||||
|
setMethod(method === "signIn" ? "signUp" : "signIn");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full flex items-center justify-center bg-background relative overflow-hidden p-4">
|
||||||
|
{/* Animated Background Blobs */}
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
rotate: [0, 90, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 20,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
className="absolute top-0 left-0 w-[500px] h-[500px] bg-primary/20 rounded-full mix-blend-multiply filter blur-[100px] opacity-50 pointer-events-none"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.1, 1],
|
||||||
|
rotate: [0, -60, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 15,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-accent/20 rounded-full mix-blend-multiply filter blur-[100px] opacity-50 pointer-events-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, type: "spring" }}
|
||||||
|
className="w-full max-w-md z-10"
|
||||||
|
>
|
||||||
|
<Card className="backdrop-blur-md bg-card/90 border-muted/50 shadow-2xl relative">
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
<CardHeader className="space-y-1 text-center">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={method}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<CardTitle className="text-2xl font-bold">
|
||||||
|
{method === "signIn" ? "Welcome Back" : "Create Account"}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{method === "signIn"
|
||||||
|
? "Enter your credentials to access your account"
|
||||||
|
: "Enter your details to get started with us"}
|
||||||
|
</CardDescription>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{method === "signIn" ? <SignInForm captchaToken={captchaToken} /> : <SignUpForm captchaToken={captchaToken} />}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col gap-4 pt-2">
|
||||||
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
|
{method === "signIn" ? "Don't have an account? " : "Already have an account? "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleMethod}
|
||||||
|
className="font-semibold text-primary hover:underline focus:outline-none"
|
||||||
|
>
|
||||||
|
{method === "signIn" ? "Sign up" : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Turnstile */}
|
||||||
|
<div className="flex flex-col justify-center items-center gap-2">
|
||||||
|
<Captcha ref={captchaRef} onSuccess={setCaptchaToken} />
|
||||||
|
{/* Reload the captcha */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCaptchaToken(null);
|
||||||
|
captchaRef.current?.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
<div className="border-px border-l border-border h-full ml-2" />
|
||||||
|
<span className="text-xs">Reload Captcha</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center w-full border-t pt-4">
|
||||||
|
<p className="text-center text-xs text-muted-foreground">
|
||||||
|
built with{" "}
|
||||||
|
<Link
|
||||||
|
href="https://better-auth.com"
|
||||||
|
className="underline hover:text-primary transition-colors"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
better-auth
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,61 +1,202 @@
|
||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@import "tw-animate-css";
|
||||||
@tailwind utilities;
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1.0000 0 0);
|
||||||
|
--foreground: oklch(0.2686 0 0);
|
||||||
|
--card: oklch(1.0000 0 0);
|
||||||
|
--card-foreground: oklch(0.2686 0 0);
|
||||||
|
--popover: oklch(1.0000 0 0);
|
||||||
|
--popover-foreground: oklch(0.2686 0 0);
|
||||||
|
--primary: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.9670 0.0029 264.5419);
|
||||||
|
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
|
||||||
|
--muted: oklch(0.9846 0.0017 247.8389);
|
||||||
|
--muted-foreground: oklch(0.5510 0.0234 264.3637);
|
||||||
|
--accent: oklch(0.9869 0.0214 95.2774);
|
||||||
|
--accent-foreground: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.9276 0.0058 264.5313);
|
||||||
|
--input: oklch(0.9276 0.0058 264.5313);
|
||||||
|
--ring: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--chart-1: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--chart-2: oklch(0.6658 0.1574 58.3183);
|
||||||
|
--chart-3: oklch(0.5553 0.1455 48.9975);
|
||||||
|
--chart-4: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--chart-5: oklch(0.4137 0.1054 45.9038);
|
||||||
|
--sidebar: oklch(0.9846 0.0017 247.8389);
|
||||||
|
--sidebar-foreground: oklch(0.2686 0 0);
|
||||||
|
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9869 0.0214 95.2774);
|
||||||
|
--sidebar-accent-foreground: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||||
|
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: Source Serif 4, serif;
|
||||||
|
--font-mono: JetBrains Mono, monospace;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--shadow-x: 1px;
|
||||||
|
--shadow-y: 2px;
|
||||||
|
--shadow-blur: 8px;
|
||||||
|
--shadow-spread: -1px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
--shadow-offset-x: 0px;
|
||||||
|
--shadow-offset-y: 4px;
|
||||||
|
--letter-spacing: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.2046 0 0);
|
||||||
|
--foreground: oklch(0.9219 0 0);
|
||||||
|
--card: oklch(0.2686 0 0);
|
||||||
|
--card-foreground: oklch(0.9219 0 0);
|
||||||
|
--popover: oklch(0.2686 0 0);
|
||||||
|
--popover-foreground: oklch(0.9219 0 0);
|
||||||
|
--primary: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.2686 0 0);
|
||||||
|
--secondary-foreground: oklch(0.9219 0 0);
|
||||||
|
--muted: oklch(0.2393 0 0);
|
||||||
|
--muted-foreground: oklch(0.7155 0 0);
|
||||||
|
--accent: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--accent-foreground: oklch(0.9243 0.1151 95.7459);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.3715 0 0);
|
||||||
|
--input: oklch(0.3715 0 0);
|
||||||
|
--ring: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--chart-1: oklch(0.8369 0.1644 84.4286);
|
||||||
|
--chart-2: oklch(0.6658 0.1574 58.3183);
|
||||||
|
--chart-3: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--chart-4: oklch(0.5553 0.1455 48.9975);
|
||||||
|
--chart-5: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--sidebar: oklch(0.1684 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.9219 0 0);
|
||||||
|
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.4732 0.1247 46.2007);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9243 0.1151 95.7459);
|
||||||
|
--sidebar-border: oklch(0.3715 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: Source Serif 4, serif;
|
||||||
|
--font-mono: JetBrains Mono, monospace;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--shadow-x: 1px;
|
||||||
|
--shadow-y: 2px;
|
||||||
|
--shadow-blur: 8px;
|
||||||
|
--shadow-spread: -1px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||||
|
--shadow-offset-x: 0px;
|
||||||
|
--shadow-offset-y: 4px;
|
||||||
|
--letter-spacing: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--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-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-mono: JetBrains Mono, monospace;
|
||||||
|
--font-serif: Source Serif 4, serif;
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--spacing: var(--spacing);
|
||||||
|
--letter-spacing: var(--letter-spacing);
|
||||||
|
--shadow-offset-y: var(--shadow-offset-y);
|
||||||
|
--shadow-offset-x: var(--shadow-offset-x);
|
||||||
|
--shadow-spread: var(--shadow-spread);
|
||||||
|
--shadow-blur: var(--shadow-blur);
|
||||||
|
--shadow-opacity: var(--shadow-opacity);
|
||||||
|
--color-shadow-color: var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
* {
|
||||||
--background: 20 14.3% 4.1%;
|
@apply border-border outline-ring/50;
|
||||||
--foreground: 60 9.1% 97.8%;
|
}
|
||||||
--card: 20 14.3% 4.1%;
|
body {
|
||||||
--card-foreground: 60 9.1% 97.8%;
|
@apply bg-background text-foreground;
|
||||||
--popover: 20 14.3% 4.1%;
|
letter-spacing: var(--tracking-normal);
|
||||||
--popover-foreground: 60 9.1% 97.8%;
|
}
|
||||||
--primary: 20.5 90.2% 48.2%;
|
}
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
|
||||||
--secondary: 12 6.5% 15.1%;
|
|
||||||
--secondary-foreground: 60 9.1% 97.8%;
|
|
||||||
--muted: 12 6.5% 15.1%;
|
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
|
||||||
--accent: 12 6.5% 15.1%;
|
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
|
||||||
--border: 12 6.5% 15.1%;
|
|
||||||
--input: 12 6.5% 15.1%;
|
|
||||||
--ring: 20.5 90.2% 48.2%;
|
|
||||||
--radius: 0.75rem;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: 20 14.3% 4.1%;
|
|
||||||
--foreground: 60 9.1% 97.8%;
|
|
||||||
--card: 20 14.3% 4.1%;
|
|
||||||
--card-foreground: 60 9.1% 97.8%;
|
|
||||||
--popover: 20 14.3% 4.1%;
|
|
||||||
--popover-foreground: 60 9.1% 97.8%;
|
|
||||||
--primary: 20.5 90.2% 48.2%;
|
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
|
||||||
--secondary: 12 6.5% 15.1%;
|
|
||||||
--secondary-foreground: 60 9.1% 97.8%;
|
|
||||||
--muted: 12 6.5% 15.1%;
|
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
|
||||||
--accent: 12 6.5% 15.1%;
|
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
|
||||||
--border: 12 6.5% 15.1%;
|
|
||||||
--input: 12 6.5% 15.1%;
|
|
||||||
--ring: 20.5 90.2% 48.2%;
|
|
||||||
--radius: 0.75rem;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +1,57 @@
|
||||||
// app/layout.tsx
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import type {Metadata} from "next";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { ConvexClientProvider } from "@/lib/providers/Convex";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import {Public_Sans} from 'next/font/google';
|
|
||||||
import {UserProvider} from "@/contexts/user";
|
|
||||||
import Sidebar from "@/components/main/sidebar/sidebar";
|
|
||||||
import {getAuthenticatedUser} from "@/lib/auth";
|
|
||||||
import {SharedStateProvider} from "@/hooks/shared-states";
|
|
||||||
import ThemeProvider from "@/components/ui/theme-provider";
|
|
||||||
import {headers} from "next/headers";
|
|
||||||
import {Toaster} from "@/components/ui/toaster";
|
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
|
||||||
subsets: ['latin'],
|
|
||||||
display: 'swap',
|
|
||||||
variable: '--font-public-sans'
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SiPher - Where Shadows Live",
|
title: "SiPher - Don't trust us. We don't trust you.",
|
||||||
description: "Secrecy? Not here, absolutely.",
|
description: "SiPher is a platform made for communication. Secure? Maybe. Reliable? I don't think so. We don't trust you. We don't trust us. We don't trust anyone.",
|
||||||
icons: [{rel: "icon", url: "/logos/logo.png"}],
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{
|
||||||
|
url: "/assets/logo/logo-white.svg",
|
||||||
|
href: "/assets/logo/logo-white.svg",
|
||||||
|
media: "(prefers-color-scheme: dark)",
|
||||||
|
type: "image/svg+xml",
|
||||||
|
sizes: "32x32",
|
||||||
|
rel: "icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/assets/logo/logo-dark.svg",
|
||||||
|
href: "/assets/logo/logo-dark.svg",
|
||||||
|
media: "(prefers-color-scheme: light)",
|
||||||
|
type: "image/svg+xml",
|
||||||
|
sizes: "32x32",
|
||||||
|
rel: "icon"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout(
|
export default function RootLayout({
|
||||||
{
|
children,
|
||||||
children,
|
}: Readonly<{
|
||||||
}: {
|
children: React.ReactNode;
|
||||||
children: React.ReactNode & { props?: { childProp?: { segment?: string } } };
|
}>) {
|
||||||
}) {
|
|
||||||
const initialUser = await getAuthenticatedUser();
|
|
||||||
const isAuthPage = (await headers()).get("x-current-pathname")?.includes("auth");
|
|
||||||
|
|
||||||
// Auth layout
|
|
||||||
if (isAuthPage) {
|
|
||||||
return (
|
|
||||||
<html lang="en" suppressHydrationWarning>
|
|
||||||
<body className={`${publicSans.variable} font-sans antialiased`}>
|
|
||||||
<ThemeProvider>
|
|
||||||
<UserProvider initialUser={initialUser}>
|
|
||||||
{children}
|
|
||||||
</UserProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
<Toaster/>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main layout
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${publicSans.variable} font-sans antialiased`}>
|
<body
|
||||||
<ThemeProvider>
|
className="antialiased min-h-screen bg-background"
|
||||||
<UserProvider initialUser={initialUser}>
|
>
|
||||||
<SharedStateProvider>
|
<ConvexClientProvider>
|
||||||
<div className="min-h-screen flex items-center justify-center p-0 sm:p-4">
|
<ThemeProvider
|
||||||
<div className="w-full min-h-screen sm:min-h-0 sm:h-[900px] max-w-[1920px] flex bg-secondary sm:p-6">
|
attribute="class"
|
||||||
<div className="w-full h-full flex bg-background sm:rounded-lg overflow-hidden">
|
defaultTheme="system"
|
||||||
<Sidebar>
|
enableSystem
|
||||||
{children}
|
disableTransitionOnChange
|
||||||
</Sidebar>
|
>
|
||||||
</div>
|
{children}
|
||||||
</div>
|
</ThemeProvider>
|
||||||
</div>
|
<Toaster richColors />
|
||||||
</SharedStateProvider>
|
</ConvexClientProvider>
|
||||||
</UserProvider>
|
</body>
|
||||||
<Toaster/>
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
328
src/app/page.tsx
328
src/app/page.tsx
|
|
@ -1,318 +1,28 @@
|
||||||
"use client"
|
"use client"
|
||||||
import {useTheme} from "next-themes";
|
import AppSidebar from "@/components/home";
|
||||||
import Image from "next/image";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import {Feather, Search} from "lucide-react";
|
import { authClient } from "@/lib/auth/client";
|
||||||
import {useEffect, useState} from "react";
|
import { redirect } from "next/navigation";
|
||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import {CryptoManager} from "@/lib/crypto/keys";
|
|
||||||
import UpdateKey from "@/lib/crypto/helpers/updateKey";
|
|
||||||
|
|
||||||
export default function SiPher() {
|
export default function Home() {
|
||||||
const {theme, systemTheme} = useTheme();
|
const { data, error, isPending, } = authClient.useSession();
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
|
if (isPending) {
|
||||||
/** CryptoManager Alert */
|
return <div className="flex items-center justify-center h-screen w-full bg-background">
|
||||||
const [privateKeyPresent, setPrivateKeyPresent] = useState(true);
|
<Spinner className="size-10 animate-spin" />
|
||||||
const [backupPanel, setBackupPanel] = useState(false); // I still need to do this, but... ugh.
|
</div>
|
||||||
|
|
||||||
/** Consent Form states */
|
|
||||||
const [showConsentForm, setShowConsentForm] = useState(false);
|
|
||||||
const [formError, setFormError] = useState("");
|
|
||||||
|
|
||||||
/** Input states */
|
|
||||||
const [inputDisabled, setInputDisabled] = useState(false);
|
|
||||||
const [inputValue, setInputValue] = useState("");
|
|
||||||
|
|
||||||
/** Search expandability state */
|
|
||||||
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
CryptoManager.getPrivateKey().then((res) => {
|
|
||||||
if (!res) {
|
|
||||||
console.log(res)
|
|
||||||
setPrivateKeyPresent(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param search_term Either the SUUID or username (If not indexable, will return false.)
|
|
||||||
*/
|
|
||||||
const fetchUser = async (search_term: string) => {
|
|
||||||
// Search term cannot be empty
|
|
||||||
if (search_term.length <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends the requisition to the API by using native fetch.
|
|
||||||
const req = await fetch(`/api/user/search/user?uuid=${search_term}`);
|
|
||||||
|
|
||||||
// Checks if the response is ok (200) or not, if not, returns false.
|
|
||||||
if (!req.ok) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await req.json() as { exists: boolean };
|
|
||||||
// If the user does not exist, just return it
|
|
||||||
if (!user.exists) return user.exists;
|
|
||||||
|
|
||||||
setShowConsentForm(true); // Shows the confirmation to ask the other user to consent to the communication;
|
|
||||||
setInputDisabled(true); // Makes the input disabled until either the user cancels the consent form or accepts it;
|
|
||||||
return user.exists; // If everything went right and the user was found, return true
|
|
||||||
}
|
}
|
||||||
const sendRequest = async (user: string) => {
|
|
||||||
if (user.length <= 0) {
|
if (error || !data) {
|
||||||
return false;
|
return redirect(`/auth${error ? `?error=${error.cause}` : ""}`);
|
||||||
}
|
|
||||||
|
|
||||||
const req = await fetch(`/api/user/send/request`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
searchTerm: user, // SUUID or username
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!req.ok) {
|
|
||||||
const res = await req.json();
|
|
||||||
setFormError(res.hint);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {sent} = await req.json() as { sent: boolean };
|
|
||||||
// If the user does not exist, just return it
|
|
||||||
if (!sent) return sent;
|
|
||||||
|
|
||||||
return sent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTheme = () => {
|
|
||||||
if (!mounted) return "light";
|
|
||||||
if (theme === "system") {
|
|
||||||
return systemTheme === "dark" ? "dark" : "light";
|
|
||||||
}
|
|
||||||
return theme === "dark" ? "dark" : "light";
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentTheme = getTheme();
|
|
||||||
|
|
||||||
const MainPageAlerts = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AlertDialog open={showConsentForm} onOpenChange={(open) => {
|
|
||||||
if (!open) setFormError("");
|
|
||||||
}}>
|
|
||||||
<AlertDialogTrigger/>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Consent Form</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className={"flex flex-col space-y-1"}>
|
|
||||||
{
|
|
||||||
formError ? (
|
|
||||||
<span className={"text-red-500"}>{formError}</span>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
<span>
|
|
||||||
Are you sure you want to contact <span className={"font-bold"}>{inputValue}</span>?
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
By continuing, <span className={"font-bold"}>{inputValue}</span> will receive a notification to accept
|
|
||||||
it. If accepted, that user will appear on your sidebar, if rejected, you will never know about it.
|
|
||||||
</span>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel
|
|
||||||
onClick={() => {
|
|
||||||
setShowConsentForm(false);
|
|
||||||
setInputDisabled(false);
|
|
||||||
}}
|
|
||||||
>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
disabled={formError.length < 0}
|
|
||||||
onClick={() => {
|
|
||||||
sendRequest(inputValue);
|
|
||||||
setInputDisabled(false);
|
|
||||||
setShowConsentForm(false);
|
|
||||||
}}
|
|
||||||
>Continue</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
|
|
||||||
<AlertDialog open={!privateKeyPresent}>
|
|
||||||
<AlertDialogTrigger/>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Private Key Missing</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className={"flex flex-col space-y-1"}>
|
|
||||||
<span>This app could not retrieve your private key, which means it's either lost, never stored or corrupted. Want to try again or insert it from a backup?</span>
|
|
||||||
<span>You can also regenerate it if you do not have it backed up, but this would mean that you'll loose access to all old messages.</span>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel
|
|
||||||
onClick={() => {
|
|
||||||
setShowConsentForm(false);
|
|
||||||
setInputDisabled(false);
|
|
||||||
}}
|
|
||||||
>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => {
|
|
||||||
sendRequest(inputValue).then((result) => {
|
|
||||||
if (!result) setFormError("Could not send notification for whatever reason. Sorry.");
|
|
||||||
});
|
|
||||||
setInputDisabled(false);
|
|
||||||
}}
|
|
||||||
>Try Again</AlertDialogAction>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => {
|
|
||||||
UpdateKey().then((result) => {
|
|
||||||
if (result.status !== 200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPrivateKeyPresent(true)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>Regenerate</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MainPageAlerts/>
|
<AppSidebar>
|
||||||
|
<div className="flex-1 p-6 flex items-start justify-center">
|
||||||
<div
|
|
||||||
className={`relative flex-1 ${currentTheme === "dark" ? "dark" : ""} w-full bg-gradient-to-b from-background to-background/95`}>
|
|
||||||
<div className="relative flex flex-col justify-center items-center h-screen px-4 select-none space-y-8">
|
|
||||||
<div className="relative group">
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-primary/5 rounded-full blur-xl group-hover:bg-primary/10 transition-all duration-500"/>
|
|
||||||
<Image
|
|
||||||
priority
|
|
||||||
src={`/logos/logo.png`}
|
|
||||||
alt="SiPher"
|
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
draggable={false}
|
|
||||||
className="relative transform transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-2xl space-y-6 text-center">
|
|
||||||
<p className="text-lg md:text-xl font-medium leading-relaxed text-primary">
|
|
||||||
Where shadows dance and secrets nest, Silent Whisper serves as the dark sanctuary for those
|
|
||||||
who value discretion above all. Born from ancient corvid traditions, this messenger’s haven ensures
|
|
||||||
your
|
|
||||||
whispers remain unheard by all but their intended recipients.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-sm md:text-base font-medium text-muted-foreground leading-relaxed">
|
|
||||||
Like the sacred ravens of old, your messages fly through the darkness, their contents sealed by shadows
|
|
||||||
and
|
|
||||||
protected by forgotten wards. Each member of our dark fellowship is known only by their chosen name, their
|
|
||||||
true identity shrouded in mystery.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative mt-8">
|
|
||||||
<div
|
|
||||||
className={`flex items-center rounded-full transition-all duration-300 ${
|
|
||||||
isSearchExpanded
|
|
||||||
? "bg-secondary/30 backdrop-blur-sm border border-primary/20 shadow-lg"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
width: isSearchExpanded ? "240px" : "40px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={`flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full
|
|
||||||
${currentTheme === "dark" ? "hover:bg-secondary/60" : "hover:bg-primary/10"}
|
|
||||||
transition-colors duration-200`}
|
|
||||||
onClick={() => setIsSearchExpanded(!isSearchExpanded)}
|
|
||||||
>
|
|
||||||
<Search className="w-5 h-5"/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Find fellow shadows..."
|
|
||||||
className={`w-full bg-transparent focus:outline-none text-primary placeholder-primary/50
|
|
||||||
transition-all duration-300 ${isSearchExpanded ? "px-4" : "w-0 px-0"}`}
|
|
||||||
disabled={inputDisabled}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
fetchUser(inputValue).then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Feather
|
|
||||||
className={`absolute -right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-primary/30 transform rotate-45
|
|
||||||
transition-opacity duration-300 ${isSearchExpanded ? "opacity-100" : "opacity-0"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator/>
|
|
||||||
<div className={"flex flex-col w-[400px]"}>
|
|
||||||
<p className="text-lg md:text-xl font-medium leading-relaxed text-primary">
|
|
||||||
F.A.Q
|
|
||||||
</p>
|
|
||||||
<Accordion type={"single"} collapsible className={"w-full"}>
|
|
||||||
<AccordionItem value={"works"}>
|
|
||||||
<AccordionTrigger>How does this works?</AccordionTrigger>
|
|
||||||
<AccordionContent asChild>
|
|
||||||
<Link href="/about" className={"text-primary text-lg p-1"}>
|
|
||||||
Please, click here
|
|
||||||
</Link>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value={"exists"}>
|
|
||||||
<AccordionTrigger>Why does this exists?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
I made this as a CS50X final project, hence why it is not intended for real usage. (Do not use it in a
|
|
||||||
situation where you need real privacy.)
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value={"os"}>
|
|
||||||
<AccordionTrigger>Is this open-source?</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
No, not yet (As of 11/12/2024)
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppSidebar>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
"use client"
|
|
||||||
import {motion} from "framer-motion";
|
|
||||||
import {useTheme} from "next-themes";
|
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
|
||||||
import {Input} from "@/components/ui/input";
|
|
||||||
import {Label} from "@/components/ui/label";
|
|
||||||
import {Switch} from "@/components/ui/switch";
|
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import {useUser} from "@/contexts/user";
|
|
||||||
import {useState} from "react";
|
|
||||||
import {AlertTriangle, Copy, Download, Eye, EyeOff, Key, Lock, Save, User} from "lucide-react";
|
|
||||||
import {CryptoManager} from "@/lib/crypto/keys";
|
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
|
||||||
const {theme, setTheme} = useTheme();
|
|
||||||
const {user} = useUser();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [privateKeyVisible, setPrivateKeyVisible] = useState(false);
|
|
||||||
const [privateKeyData, setPrivateKeyData] = useState<{ text: string; file: File } | null>(null);
|
|
||||||
const [backupError, setBackupError] = useState("");
|
|
||||||
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: {opacity: 0, y: 20},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.6,
|
|
||||||
staggerChildren: 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: {opacity: 0, y: 20},
|
|
||||||
visible: {opacity: 1, y: 0}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="flex-1 space-y-8 p-8 pt-6"
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
variants={containerVariants}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Settings</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Manage your account settings and preferences
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue="profile" className="space-y-6">
|
|
||||||
<TabsList className="w-full justify-start">
|
|
||||||
<TabsTrigger value="profile" className="flex items-center gap-2">
|
|
||||||
<User size={16}/>
|
|
||||||
Profile
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="privacy" className="flex items-center gap-2">
|
|
||||||
<Lock size={16}/>
|
|
||||||
Privacy
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<TabsContent value="profile" className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Profile Information</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Update your profile information and settings
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="username">Username</Label>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
defaultValue={user.username}
|
|
||||||
placeholder="Your username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="suuid">Your SUUID</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="suuid"
|
|
||||||
value={user.suuid}
|
|
||||||
readOnly
|
|
||||||
className="font-mono"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(user.suuid);
|
|
||||||
}}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="privacy" className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Privacy Settings</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage your privacy and security preferences
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label>Message Encryption</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
End-to-end encryption is always enabled
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Key className="h-4 w-4 text-primary"/>
|
|
||||||
</div>
|
|
||||||
<Separator/>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label>Private Key Backup</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
View and download your private key for backup
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
const data = await CryptoManager.exportPrivateKey();
|
|
||||||
if (data) {
|
|
||||||
setPrivateKeyData(data);
|
|
||||||
setBackupError("");
|
|
||||||
} else {
|
|
||||||
setBackupError("Failed to export private key");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setBackupError("Error accessing private key");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 mr-2"/>
|
|
||||||
View Key
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
const data = await CryptoManager.exportPrivateKey();
|
|
||||||
if (data) {
|
|
||||||
const url = URL.createObjectURL(data.file);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = data.file.name;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
setBackupError("");
|
|
||||||
} else {
|
|
||||||
setBackupError("Failed to download private key");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setBackupError("Error downloading private key");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 mr-2"/>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{backupError && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTriangle className="h-4 w-4"/>
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{backupError}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{privateKeyData && (
|
|
||||||
<Card className="mt-4 w-full">
|
|
||||||
<CardHeader className="py-3">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<CardTitle className="text-sm">Private Key</CardTitle>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(privateKeyData.text);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4"/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setPrivateKeyData(null);
|
|
||||||
setPrivateKeyVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EyeOff className="h-4 w-4"/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="max-w-full overflow-hidden rounded-lg bg-secondary/50">
|
|
||||||
<pre className="p-4 text-xs overflow-x-auto whitespace-pre-wrap break-all">
|
|
||||||
{privateKeyData.text}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Separator/>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label>Allow Message Requests</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Receive message requests from other users
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch defaultChecked/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<AlertTriangle className="h-4 w-4"/>
|
|
||||||
<AlertTitle>Private Key Management</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Your private key is stored securely in your browser.
|
|
||||||
Make sure to back it up to avoid losing access to your messages.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</TabsContent>
|
|
||||||
</motion.div>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={itemVariants}
|
|
||||||
className="flex justify-end"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="w-32"
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => {
|
|
||||||
setLoading(true);
|
|
||||||
// Simulate saving
|
|
||||||
setTimeout(() => setLoading(false), 1000);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<motion.div
|
|
||||||
animate={{rotate: 360}}
|
|
||||||
transition={{duration: 1, repeat: Infinity, ease: "linear"}}
|
|
||||||
>
|
|
||||||
<Save className="h-4 w-4 mr-2"/>
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
"Save Changes"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
72
src/components/home/index.tsx
Normal file
72
src/components/home/index.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import LogoIcon from "@/components/ui/logo-icon";
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarInset,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarTrigger
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
|
||||||
|
const SidebarItems = [
|
||||||
|
{
|
||||||
|
id: "home",
|
||||||
|
// The icon of the home item is the same as the logo
|
||||||
|
icon: <LogoIcon />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main component for the homepage. This component is used to wrap all the components of any page.
|
||||||
|
* It also is the controller for everything on the app, including going to other pages, showing conversations and other.
|
||||||
|
* @param children - The children to be rendered in the sidebar inset
|
||||||
|
*/
|
||||||
|
export default function AppSidebar({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<SidebarProvider
|
||||||
|
style={{
|
||||||
|
"--sidebar-width": "5rem",
|
||||||
|
"--sidebar-width-mobile": "8rem",
|
||||||
|
} as React.CSSProperties}
|
||||||
|
defaultOpen={true}
|
||||||
|
>
|
||||||
|
<Sidebar variant="inset" collapsible="offcanvas" className="border-r-0">
|
||||||
|
<SidebarHeader className="flex items-center justify-center py-2 pt-4 w-full">
|
||||||
|
<Button variant="ghost" size="icon-lg" className="border border-border rounded-lg hover:bg-accent transition-colors">
|
||||||
|
<LogoIcon className="size-8" />
|
||||||
|
</Button>
|
||||||
|
</SidebarHeader>
|
||||||
|
<Separator className="my-1.5" />
|
||||||
|
<SidebarContent className="px-1.5">
|
||||||
|
<SidebarMenu>
|
||||||
|
{SidebarItems.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.id} className="flex items-center justify-center py-2">
|
||||||
|
<Button variant="ghost" size="icon-lg" className="hover:bg-accent transition-colors">
|
||||||
|
{item.icon}
|
||||||
|
</Button>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
<div className="flex flex-col flex-1 min-h-screen">
|
||||||
|
<header className="flex items-center justify-between md:justify-center gap-2 px-4 py-1.5 md:border-none border-b border-border backdrop-blur sticky top-0 z-10">
|
||||||
|
<div className="flex items-center gap-2 md:hidden">
|
||||||
|
<SidebarTrigger className="size-9" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-sm font-semibold">Your Header Title</h2>
|
||||||
|
<div className="w-9 md:hidden" /> {/* Spacer for centering on mobile */}
|
||||||
|
</header>
|
||||||
|
<SidebarInset className="mr-0 mb-0 border-none flex-1 rounded-l-lg">
|
||||||
|
<div className="w-full h-full bg-background border-border border rounded-l-lg rounded-bl-none overflow-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SidebarInset>
|
||||||
|
</div>
|
||||||
|
</SidebarProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
// hooks/useRealtime.ts
|
|
||||||
import {Dispatch, SetStateAction, useCallback, useEffect} from 'react'
|
|
||||||
import {createBrowserClient} from '@/lib/supabase/browser'
|
|
||||||
import {useUser} from '@/contexts/user'
|
|
||||||
|
|
||||||
interface UseRealtimeProps {
|
|
||||||
setThreads: Dispatch<SetStateAction<SiPher.Thread[]>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRealtime({setThreads}: UseRealtimeProps) {
|
|
||||||
const supabase = createBrowserClient();
|
|
||||||
const {user, updateUser} = useUser();
|
|
||||||
|
|
||||||
const fetchAndUpdateThreads = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/user/get/threads");
|
|
||||||
if (response.ok) {
|
|
||||||
const {threads} = await response.json();
|
|
||||||
setThreads(threads);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching threads:', error);
|
|
||||||
}
|
|
||||||
}, [setThreads])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
const userUpdate = supabase
|
|
||||||
.channel("request updates")
|
|
||||||
.on("postgres_changes", {
|
|
||||||
event: "*",
|
|
||||||
schema: 'public',
|
|
||||||
table: 'users',
|
|
||||||
filter: `uuid=eq.${user.uuid}`,
|
|
||||||
}, async (payload) => {
|
|
||||||
console.log(payload)
|
|
||||||
if (payload.eventType === "UPDATE") {
|
|
||||||
// This will also handle updates for the threads, but only for the user that accepted the request.
|
|
||||||
// Why? Because the function that creates the thread will also update the current user request field and remove
|
|
||||||
// the corresponding request.
|
|
||||||
if (payload.new.requests !== payload.old.requests) {
|
|
||||||
updateUser({
|
|
||||||
...user,
|
|
||||||
requests: payload.new.requests
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (payload.eventType === "DELETE") {
|
|
||||||
console.log(`Payload from delete: \n${payload}`)
|
|
||||||
updateUser({
|
|
||||||
...user,
|
|
||||||
//@ts-expect-error
|
|
||||||
requests: payload.new
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).subscribe()
|
|
||||||
|
|
||||||
const threadUpdate = supabase
|
|
||||||
.channel("thread updates")
|
|
||||||
.on("postgres_changes", {
|
|
||||||
event: "*",
|
|
||||||
schema: 'public',
|
|
||||||
table: "thread_participants",
|
|
||||||
filter: `user_uuid=eq.${user.uuid}`
|
|
||||||
}, async (payload) => {
|
|
||||||
if (payload.new !== payload.old) {
|
|
||||||
await fetchAndUpdateThreads();
|
|
||||||
}
|
|
||||||
}).subscribe()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
threadUpdate.unsubscribe()
|
|
||||||
userUpdate.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [user?.uuid, fetchAndUpdateThreads, supabase, updateUser, user]);
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
"use client"
|
|
||||||
import React from 'react'
|
|
||||||
import {Button} from "@/components/ui/button"
|
|
||||||
import {HamburgerMenuIcon} from "@radix-ui/react-icons"
|
|
||||||
import {useTheme} from "next-themes"
|
|
||||||
import Image from "next/image"
|
|
||||||
import {useUIState} from "@/hooks/shared-states"
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const MobileHeader: React.FC = () => {
|
|
||||||
const {setIsDrawerOpen} = useUIState()
|
|
||||||
const {theme, systemTheme} = useTheme()
|
|
||||||
|
|
||||||
const getTheme = () => {
|
|
||||||
if (theme === "system") {
|
|
||||||
switch (systemTheme) {
|
|
||||||
case "dark":
|
|
||||||
return "dark"
|
|
||||||
default:
|
|
||||||
return "light"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return theme === "dark" ? "dark" : "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoSrc = getTheme() === 'dark' ? '/logos/logo-light.png' : '/logos/logo.png'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 lg:hidden pb-10">
|
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-background">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setIsDrawerOpen(true)}
|
|
||||||
className="rounded-full"
|
|
||||||
>
|
|
||||||
<HamburgerMenuIcon className="w-6 h-6"/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center flex-1">
|
|
||||||
<Link href="/" className="block">
|
|
||||||
<Image
|
|
||||||
src={logoSrc}
|
|
||||||
alt="Logo"
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
className="w-12 h-12 cursor-pointer rounded-full hover:bg-secondary/20"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Empty div to maintain center alignment */}
|
|
||||||
<div className="w-10 mb-8"/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MobileHeader
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
import React, {useCallback, useEffect, useState} from "react";
|
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
|
||||||
import {Avatar, AvatarFallback} from "@/components/ui/avatar";
|
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area";
|
|
||||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
|
||||||
import {Check, LogOut, Mail, MailPlus, X} from "lucide-react";
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {useRealtime} from "@/components/main/realtime";
|
|
||||||
import {useUser} from "@/contexts/user";
|
|
||||||
import {usePathname} from "next/navigation";
|
|
||||||
import {useSharedState} from "@/hooks/shared-states";
|
|
||||||
|
|
||||||
interface RightSidebarContentProps {
|
|
||||||
isDarkMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RightSidebarContent(
|
|
||||||
{
|
|
||||||
isDarkMode,
|
|
||||||
}: RightSidebarContentProps) {
|
|
||||||
|
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const {threads, setThreads} = useSharedState();
|
|
||||||
useRealtime({setThreads});
|
|
||||||
|
|
||||||
const {user} = useUser();
|
|
||||||
const {username, suuid, requests = []} = user;
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const pendingRequests = requests?.length ?? 0;
|
|
||||||
|
|
||||||
const fetchThreads = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const req = await fetch("/api/user/get/threads")
|
|
||||||
if (req.ok) {
|
|
||||||
const {threads} = await req.json() as { threads: SiPher.Thread[] | [] }
|
|
||||||
setThreads(threads)
|
|
||||||
} else {
|
|
||||||
setThreads([])
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
setThreads([])
|
|
||||||
}
|
|
||||||
}, [setThreads]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchThreads();
|
|
||||||
}, [fetchThreads]);
|
|
||||||
|
|
||||||
const handleAccept = async (request: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/user/create/thread", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({participant: request}),
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
fetchThreads();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error accepting request:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={`flex flex-col h-full w-[240px]`}>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip open={copied} onOpenChange={setCopied}>
|
|
||||||
<TooltipTrigger/>
|
|
||||||
<TooltipContent arrowPadding={10} className={"p-2 shadow-cyan-950 shadow-md"}>
|
|
||||||
Copied SUUID to clipboard!
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
setCopied(true)
|
|
||||||
navigator.clipboard.writeText(suuid)
|
|
||||||
}}
|
|
||||||
className={`flex items-center p-3 m-2 ${isDarkMode ? "hover:bg-accent/90" : "hover:bg-secondary/20"} rounded-full transition-colors duration-200 cursor-pointer select-none`}>
|
|
||||||
<Avatar className="w-12 h-12 mr-3">
|
|
||||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<h3 className={`font-semibold text-[17px] ${isDarkMode ? "text-white" : "text-black"}`}>{username}</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">${suuid}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-2"/>
|
|
||||||
<ScrollArea className="flex-grow max-h-[500px] px-4 py-4">
|
|
||||||
<nav>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<div className={"flex flex-row items-center w-full justify-start text-[17px]"}>
|
|
||||||
{
|
|
||||||
(user.requests?.length ?? 0) > 0 ? (
|
|
||||||
<MailPlus className="w-8 h-8 mr-3 p-1"/>
|
|
||||||
) : (
|
|
||||||
<Mail className="w-8 h-8 mr-3 p-1"/>
|
|
||||||
)}
|
|
||||||
Requests
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="px-4 py-1 w-56" side={"right"}>
|
|
||||||
<div className={"flex flex-row w-full justify-between items-center select-none"}>
|
|
||||||
<p>User</p>
|
|
||||||
<p>Decline | Accept</p>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
pendingRequests > 0 && requests!.map((request, item) => {
|
|
||||||
return (
|
|
||||||
<div key={item} className={"flex flex-col w-full"}>
|
|
||||||
<Separator className="my-2"/>
|
|
||||||
<div key={item} className={"flex flex-row space-x-2 w-full items-center"}>
|
|
||||||
<p className={"text-secondary-foreground"}>{request}</p>
|
|
||||||
<div className={"flex flex-row justify-end space-x-1 w-full"}>
|
|
||||||
<Button size={"icon"} className={"bg-red-500"}>
|
|
||||||
<X className={"w-4 h-4"}/>
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
handleAccept(request)
|
|
||||||
}} size={"icon"} className={"bg-green-500"}>
|
|
||||||
<Check className={"w-4 h-4"}/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}) || (
|
|
||||||
<p>Nothing new here</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Separator className="my-2"/>
|
|
||||||
{threads && threads.length > 0 ? (
|
|
||||||
threads.map((thread, index) => {
|
|
||||||
// Gets the user's username instead of the SUUID to use as a recognizable user.
|
|
||||||
const otherUser = thread.participants.filter((user) => user !== username)[0];
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
<Link href={`/${thread.thread_id}`} passHref>
|
|
||||||
<Button
|
|
||||||
variant={pathname.replace("/", "") === thread.thread_id ? "secondary" : "ghost"}
|
|
||||||
className={`w-full justify-start text-[17px] p-2`}>
|
|
||||||
<Avatar className="w-8 h-8 mr-3">
|
|
||||||
<AvatarFallback>{otherUser.charAt(0).toUpperCase()}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
{otherUser}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p>No threads available</p>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</ScrollArea>
|
|
||||||
<div className="p-3 space-y-3">
|
|
||||||
<Separator/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-[17px] py-2 text-primary"
|
|
||||||
onClick={() => window.location.href = "/settings"}
|
|
||||||
>
|
|
||||||
<GearIcon className="w-4 h-4 mr-3"/>
|
|
||||||
Settings
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
fetch("/api/auth/logout", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
window.location.href = "/auth/login"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}} variant="outline" className="w-full justify-start text-[17px] py-2 text-destructive">
|
|
||||||
<LogOut className="w-4 h-4 mr-3"/>
|
|
||||||
Log Out
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
"use client"
|
|
||||||
import React from "react"
|
|
||||||
import Link from "next/link"
|
|
||||||
import {AnimatePresence, motion} from "framer-motion"
|
|
||||||
import {X} from "lucide-react"
|
|
||||||
import {Button} from "@/components/ui/button"
|
|
||||||
import Image from "next/image";
|
|
||||||
import MobileHeader from "@/components/main/sidebar/mobile";
|
|
||||||
import {useRefs, useUIState} from "@/hooks/shared-states";
|
|
||||||
import {useTheme} from "next-themes";
|
|
||||||
import RightSidebarContent from "@/components/main/sidebar/rightsidebar";
|
|
||||||
|
|
||||||
type SidebarProps = {
|
|
||||||
children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sidebar(
|
|
||||||
{
|
|
||||||
children
|
|
||||||
}: SidebarProps
|
|
||||||
) {
|
|
||||||
const {theme, systemTheme} = useTheme();
|
|
||||||
|
|
||||||
const {isDrawerOpen, setIsDrawerOpen} = useUIState();
|
|
||||||
const {drawerRef} = useRefs();
|
|
||||||
|
|
||||||
const isDarkMode = theme === "system"
|
|
||||||
? systemTheme === "dark"
|
|
||||||
: theme === "dark"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MobileHeader/>
|
|
||||||
<aside
|
|
||||||
className={`hidden lg:flex flex-col items-end h-screen max-h-[900px] sticky top-0 border-r border-border ${
|
|
||||||
isDarkMode ? "bg-background" : "white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-items-start w-[240px] mt-1.5 hover:scale-105 transition-all duration-300">
|
|
||||||
<Link href={"/"} passHref className={"flex flex-row items-center ml-1.5"}>
|
|
||||||
<Image
|
|
||||||
src={isDarkMode ? "/logos/logo.png" : "/logos/logo-light.png"}
|
|
||||||
alt="SiPher Space"
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="w-16 h-16 cursor-pointer rounded-full antialiased"
|
|
||||||
/>
|
|
||||||
<p className={"text-center text-xl font-bold antialiased"}>SiPher</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<RightSidebarContent
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
<AnimatePresence>
|
|
||||||
{isDrawerOpen && (
|
|
||||||
<motion.div
|
|
||||||
ref={drawerRef}
|
|
||||||
initial={{x: '-100%'}}
|
|
||||||
animate={{x: 0}}
|
|
||||||
exit={{x: '-100%'}}
|
|
||||||
transition={{type: 'tween'}}
|
|
||||||
className={`fixed inset-y-0 left-0 w-64 ${
|
|
||||||
isDarkMode ? "bg-background" : "bg-white"
|
|
||||||
} border-r border-border shadow-lg z-50 lg:hidden`}
|
|
||||||
>
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
onClick={() => setIsDrawerOpen(false)}
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5"/>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
</Button>
|
|
||||||
<RightSidebarContent
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<div className={"flex-1 overflow-auto"}>{
|
|
||||||
children ?? null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sidebar
|
|
||||||
20
src/components/mode-toggle.tsx
Normal file
20
src/components/mode-toggle.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
93
src/components/socket-test.tsx
Normal file
93
src/components/socket-test.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { io, Socket } from "socket.io-client"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
|
||||||
|
export default function SocketTest() {
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null)
|
||||||
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
const [messages, setMessages] = useState<string[]>([])
|
||||||
|
const [inputMessage, setInputMessage] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize Socket.IO client
|
||||||
|
const socketInstance = io()
|
||||||
|
|
||||||
|
socketInstance.on("connect", () => {
|
||||||
|
console.log("Connected to Socket.IO:", socketInstance.id)
|
||||||
|
setIsConnected(true)
|
||||||
|
setMessages(prev => [...prev, `✅ Connected: ${socketInstance.id}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
socketInstance.on("disconnect", (reason) => {
|
||||||
|
console.log("Disconnected:", reason)
|
||||||
|
setIsConnected(false)
|
||||||
|
setMessages(prev => [...prev, `❌ Disconnected: ${reason}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
socketInstance.on("message", (data) => {
|
||||||
|
console.log("Message received:", data)
|
||||||
|
setMessages(prev => [...prev, `📩 Received: ${data}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
setSocket(socketInstance)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socketInstance.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (socket && inputMessage.trim()) {
|
||||||
|
socket.emit("message", inputMessage)
|
||||||
|
setMessages(prev => [...prev, `📤 Sent: ${inputMessage}`])
|
||||||
|
setInputMessage("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Socket.IO Test Client</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Status: {isConnected ? (
|
||||||
|
<span className="text-green-600 font-semibold">🟢 Connected</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-red-600 font-semibold">🔴 Disconnected</span>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter message..."
|
||||||
|
value={inputMessage}
|
||||||
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
|
||||||
|
disabled={!isConnected}
|
||||||
|
/>
|
||||||
|
<Button onClick={sendMessage} disabled={!isConnected}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4 h-64 overflow-y-auto bg-muted/20">
|
||||||
|
<div className="space-y-1 font-mono text-sm">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No messages yet...</p>
|
||||||
|
) : (
|
||||||
|
messages.map((msg, idx) => (
|
||||||
|
<p key={idx} className="text-xs">{msg}</p>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
12
src/components/theme-provider.tsx
Normal file
12
src/components/theme-provider.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
||||||
import {ChevronDown} from "lucide-react"
|
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AccordionItem.displayName = "AccordionItem"
|
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
||||||
>(({className, children, ...props}, ref) => (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"/>
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
))
|
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
||||||
>(({className, children, ...props}, ref) => (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
))
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
|
||||||
import {buttonVariants} from "@/components/ui/button"
|
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPrimitive.Overlay
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay/>
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
))
|
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogDescription.displayName =
|
|
||||||
AlertDialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPrimitive.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants(), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AlertDialogPrimitive.Cancel
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({variant: "outline"}),
|
|
||||||
"mt-2 sm:mt-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +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 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
||||||
>(({className, variant, ...props}, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({variant}), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDescription.displayName = "AlertDescription"
|
|
||||||
|
|
||||||
export {Alert, AlertTitle, AlertDescription}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
ref={ref}
|
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
||||||
|
|
||||||
export {Avatar, AvatarImage, AvatarFallback}
|
|
||||||
83
src/components/ui/button-group.tsx
Normal file
83
src/components/ui/button-group.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
|
const buttonGroupVariants = cva(
|
||||||
|
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal:
|
||||||
|
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||||
|
vertical:
|
||||||
|
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function ButtonGroup({
|
||||||
|
className,
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="button-group"
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonGroupText({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "div"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonGroupSeparator({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
data-slot="button-group-separator"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ButtonGroup,
|
||||||
|
ButtonGroupSeparator,
|
||||||
|
ButtonGroupText,
|
||||||
|
buttonGroupVariants,
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,33 @@
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import * as React from "react"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
"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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "h-9 w-9",
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
"icon-xl": "size-12",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -34,24 +37,25 @@ const buttonVariants = cva(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
function Button({
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
className,
|
||||||
VariantProps<typeof buttonVariants> {
|
variant,
|
||||||
asChild?: boolean
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export { Button, buttonVariants }
|
||||||
({className, variant, size, asChild = false, ...props}, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={cn(buttonVariants({variant, size, className}))}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export {Button, buttonVariants}
|
|
||||||
|
|
|
||||||
216
src/components/ui/calendar.tsx
Normal file
216
src/components/ui/calendar.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString("default", { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"select-none font-medium",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"select-none w-(--cell-size)",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] select-none text-muted-foreground",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
|
props.showWeekNumber
|
||||||
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||||
|
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"rounded-l-md bg-accent",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
|
today: cn(
|
||||||
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: CalendarDayButton,
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
24
src/components/ui/captcha.tsx
Normal file
24
src/components/ui/captcha.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
|
||||||
|
export interface CaptchaRef {
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Captcha = forwardRef<CaptchaRef, { onSuccess: (token: string) => void }>(
|
||||||
|
({ onSuccess }, ref) => {
|
||||||
|
const turnstileRef = useRef<TurnstileInstance>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reset: () => {
|
||||||
|
turnstileRef.current?.reset();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return <Turnstile ref={turnstileRef} siteKey='0x4AAAAAACDEvU2-PUzwj3L0' onSuccess={onSuccess} />
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Captcha.displayName = 'Captcha';
|
||||||
|
|
||||||
|
export default Captcha;
|
||||||
|
|
@ -1,76 +1,92 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card"
|
||||||
<div
|
className={cn(
|
||||||
ref={ref}
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className={cn(
|
className
|
||||||
"rounded-xl border bg-card text-card-foreground shadow",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
Card.displayName = "Card"
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card-header"
|
||||||
<div
|
className={cn(
|
||||||
ref={ref}
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
CardHeader.displayName = "CardHeader"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card-title"
|
||||||
<div
|
className={cn("leading-none font-semibold", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card-description"
|
||||||
<div
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card-action"
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
className={cn(
|
||||||
))
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
CardContent.displayName = "CardContent"
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({className, ...props}, ref) => (
|
data-slot="card-content"
|
||||||
<div
|
className={cn("px-6", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}
|
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,
|
||||||
|
}
|
||||||
|
|
|
||||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
184
src/components/ui/command.tsx
Normal file
184
src/components/ui/command.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
252
src/components/ui/context-menu.tsx
Normal file
252
src/components/ui/context-menu.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ContextMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
|
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
data-slot="context-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 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" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
data-slot="context-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-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
data-slot="context-menu-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 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot="context-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 ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="context-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">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
data-slot="context-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">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
}
|
||||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import {Check, ChevronRight, Circle} from "lucide-react"
|
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({className, inset, children, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto"/>
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({className, sideOffset = 4, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({className, inset, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({className, children, checked, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4"/>
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({className, children, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current"/>
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({className, inset, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
}
|
|
||||||
104
src/components/ui/empty.tsx
Normal file
104
src/components/ui/empty.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty"
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-header"
|
||||||
|
className={cn(
|
||||||
|
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMediaVariants = cva(
|
||||||
|
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function EmptyMedia({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-icon"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-title"
|
||||||
|
className={cn("text-lg font-medium tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-content"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Empty,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyTitle,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyMedia,
|
||||||
|
}
|
||||||
248
src/components/ui/field.tsx
Normal file
248
src/components/ui/field.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
|
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||||
|
return (
|
||||||
|
<fieldset
|
||||||
|
data-slot="field-set"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-6",
|
||||||
|
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLegend({
|
||||||
|
className,
|
||||||
|
variant = "legend",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||||
|
return (
|
||||||
|
<legend
|
||||||
|
data-slot="field-legend"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"mb-3 font-medium",
|
||||||
|
"data-[variant=legend]:text-base",
|
||||||
|
"data-[variant=label]:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-group"
|
||||||
|
className={cn(
|
||||||
|
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldVariants = cva(
|
||||||
|
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||||
|
horizontal: [
|
||||||
|
"flex-row items-center",
|
||||||
|
"[&>[data-slot=field-label]]:flex-auto",
|
||||||
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
],
|
||||||
|
responsive: [
|
||||||
|
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
||||||
|
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||||
|
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "vertical",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="field"
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(fieldVariants({ orientation }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-content"
|
||||||
|
className={cn(
|
||||||
|
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Label>) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="field-label"
|
||||||
|
className={cn(
|
||||||
|
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||||
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||||
|
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-label"
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="field-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||||
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||||
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-separator"
|
||||||
|
data-content={!!children}
|
||||||
|
className={cn(
|
||||||
|
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
|
{children && (
|
||||||
|
<span
|
||||||
|
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||||
|
data-slot="field-separator-content"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldError({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
errors,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
errors?: Array<{ message?: string } | undefined>
|
||||||
|
}) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (children) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueErrors = [
|
||||||
|
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (uniqueErrors?.length == 1) {
|
||||||
|
return uniqueErrors[0]?.message
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||||
|
{uniqueErrors.map(
|
||||||
|
(error, index) =>
|
||||||
|
error?.message && <li key={index}>{error.message}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}, [children, errors])
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
data-slot="field-error"
|
||||||
|
className={cn("text-destructive text-sm font-normal", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Field,
|
||||||
|
FieldLabel,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLegend,
|
||||||
|
FieldSeparator,
|
||||||
|
FieldSet,
|
||||||
|
FieldContent,
|
||||||
|
FieldTitle,
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
({className, type, ...props}, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={type}
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"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",
|
||||||
className
|
"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",
|
||||||
ref={ref}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
)
|
/>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export {Input}
|
export { Input }
|
||||||
|
|
|
||||||
193
src/components/ui/item.tsx
Normal file
193
src/components/ui/item.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
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"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
|
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="list"
|
||||||
|
data-slot="item-group"
|
||||||
|
className={cn("group/item-group flex flex-col", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
data-slot="item-separator"
|
||||||
|
orientation="horizontal"
|
||||||
|
className={cn("my-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemVariants = cva(
|
||||||
|
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline: "border-border",
|
||||||
|
muted: "bg-muted/50",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "p-4 gap-4 ",
|
||||||
|
sm: "py-3 px-4 gap-2.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Item({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "div"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="item"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(itemVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemMediaVariants = cva(
|
||||||
|
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
image:
|
||||||
|
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function ItemMedia({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-media"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(itemMediaVariants({ variant, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-content"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-title"
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="item-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||||
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-actions"
|
||||||
|
className={cn("flex items-center gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-header"
|
||||||
|
className={cn(
|
||||||
|
"flex basis-full items-center justify-between gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="item-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex basis-full items-center justify-between gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Item,
|
||||||
|
ItemMedia,
|
||||||
|
ItemContent,
|
||||||
|
ItemActions,
|
||||||
|
ItemGroup,
|
||||||
|
ItemSeparator,
|
||||||
|
ItemTitle,
|
||||||
|
ItemDescription,
|
||||||
|
ItemHeader,
|
||||||
|
ItemFooter,
|
||||||
|
}
|
||||||
28
src/components/ui/kbd.tsx
Normal file
28
src/components/ui/kbd.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||||
|
return (
|
||||||
|
<kbd
|
||||||
|
data-slot="kbd"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
||||||
|
"[&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<kbd
|
||||||
|
data-slot="kbd-group"
|
||||||
|
className={cn("inline-flex items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Kbd, KbdGroup }
|
||||||
|
|
@ -2,25 +2,23 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import {cva, type VariantProps} from "class-variance-authority"
|
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const labelVariants = cva(
|
function Label({
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
export { Label }
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
||||||
VariantProps<typeof labelVariants>
|
|
||||||
>(({className, ...props}, ref) => (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(labelVariants(), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export {Label}
|
|
||||||
|
|
|
||||||
17
src/components/ui/logo-icon.tsx
Normal file
17
src/components/ui/logo-icon.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Get the logo SVG and return it as a React component
|
||||||
|
import logoDark from "@/assets/logo/logo-dark.svg";
|
||||||
|
import logoLight from "@/assets/logo/logo-white.svg";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
export default function LogoIcon(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
return theme === "dark" ? <img src={logoLight.src} alt="Logo" className={cn("size-6", className)} /> : <img src={logoDark.src} alt="Logo" className={cn("size-6", className)} />;
|
||||||
|
}
|
||||||
276
src/components/ui/menubar.tsx
Normal file
276
src/components/ui/menubar.tsx
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Menubar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Root
|
||||||
|
data-slot="menubar"
|
||||||
|
className={cn(
|
||||||
|
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||||
|
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||||
|
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||||
|
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Trigger
|
||||||
|
data-slot="menubar-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
alignOffset = -4,
|
||||||
|
sideOffset = 8,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<MenubarPortal>
|
||||||
|
<MenubarPrimitive.Content
|
||||||
|
data-slot="menubar-content"
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenubarPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Item
|
||||||
|
data-slot="menubar-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 MenubarCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.CheckboxItem
|
||||||
|
data-slot="menubar-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.RadioItem
|
||||||
|
data-slot="menubar-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Label
|
||||||
|
data-slot="menubar-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Separator
|
||||||
|
data-slot="menubar-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="menubar-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||||
|
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.SubTrigger
|
||||||
|
data-slot="menubar-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 flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
|
</MenubarPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.SubContent
|
||||||
|
data-slot="menubar-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-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Menubar,
|
||||||
|
MenubarPortal,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarTrigger,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarShortcut,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarRadioGroup,
|
||||||
|
MenubarRadioItem,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarSubContent,
|
||||||
|
}
|
||||||
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
|
|
@ -3,46 +3,56 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
function ScrollArea({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
children,
|
||||||
>(({className, children, ...props}, ref) => (
|
...props
|
||||||
<ScrollAreaPrimitive.Root
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn("relative overflow-hidden", className)}
|
<ScrollAreaPrimitive.Root
|
||||||
{...props}
|
data-slot="scroll-area"
|
||||||
>
|
className={cn("relative", className)}
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
{...props}
|
||||||
{children}
|
>
|
||||||
</ScrollAreaPrimitive.Viewport>
|
<ScrollAreaPrimitive.Viewport
|
||||||
<ScrollBar/>
|
data-slot="scroll-area-viewport"
|
||||||
<ScrollAreaPrimitive.Corner/>
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
</ScrollAreaPrimitive.Root>
|
>
|
||||||
))
|
{children}
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
function ScrollBar({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
orientation = "vertical",
|
||||||
>(({className, orientation = "vertical", ...props}, ref) => (
|
...props
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
ref={ref}
|
return (
|
||||||
orientation={orientation}
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
className={cn(
|
data-slot="scroll-area-scrollbar"
|
||||||
"flex touch-none select-none transition-colors",
|
orientation={orientation}
|
||||||
orientation === "vertical" &&
|
className={cn(
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
"flex touch-none p-px transition-colors select-none",
|
||||||
orientation === "horizontal" &&
|
orientation === "vertical" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
className
|
orientation === "horizontal" &&
|
||||||
)}
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border"/>
|
{...props}
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
>
|
||||||
))
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {ScrollArea, ScrollBar}
|
export { ScrollArea, ScrollBar }
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,26 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{className, orientation = "horizontal", decorative = true, ...props},
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
<SeparatorPrimitive.Root
|
||||||
<SeparatorPrimitive.Root
|
data-slot="separator"
|
||||||
ref={ref}
|
decorative={decorative}
|
||||||
decorative={decorative}
|
orientation={orientation}
|
||||||
orientation={orientation}
|
className={cn(
|
||||||
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",
|
||||||
"shrink-0 bg-border",
|
className
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
)
|
|
||||||
)
|
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export {Separator}
|
export { Separator }
|
||||||
|
|
|
||||||
139
src/components/ui/sheet.tsx
Normal file
139
src/components/ui/sheet.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-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 SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary 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">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
726
src/components/ui/sidebar.tsx
Normal file
726
src/components/ui/sidebar.tsx
Normal file
|
|
@ -0,0 +1,726 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "@/components/ui/sheet"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
|
|
||||||
|
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||||
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||||
|
const SIDEBAR_WIDTH = "16rem"
|
||||||
|
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||||
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|
||||||
|
type SidebarContextProps = {
|
||||||
|
state: "expanded" | "collapsed"
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
openMobile: boolean
|
||||||
|
setOpenMobile: (open: boolean) => void
|
||||||
|
isMobile: boolean
|
||||||
|
toggleSidebar: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||||
|
|
||||||
|
function useSidebar() {
|
||||||
|
const context = React.useContext(SidebarContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarProvider({
|
||||||
|
defaultOpen = true,
|
||||||
|
open: openProp,
|
||||||
|
onOpenChange: setOpenProp,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
defaultOpen?: boolean
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const [openMobile, setOpenMobile] = React.useState(false)
|
||||||
|
|
||||||
|
// This is the internal state of the sidebar.
|
||||||
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
|
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||||
|
const open = openProp ?? _open
|
||||||
|
const setOpen = React.useCallback(
|
||||||
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
|
const openState = typeof value === "function" ? value(open) : value
|
||||||
|
if (setOpenProp) {
|
||||||
|
setOpenProp(openState)
|
||||||
|
} else {
|
||||||
|
_setOpen(openState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||||
|
},
|
||||||
|
[setOpenProp, open]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
const toggleSidebar = React.useCallback(() => {
|
||||||
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||||
|
}, [isMobile, setOpen, setOpenMobile])
|
||||||
|
|
||||||
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||||
|
(event.metaKey || event.ctrlKey)
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
toggleSidebar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}, [toggleSidebar])
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = open ? "expanded" : "collapsed"
|
||||||
|
|
||||||
|
const contextValue = React.useMemo<SidebarContextProps>(
|
||||||
|
() => ({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
}),
|
||||||
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={contextValue}>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-wrapper"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH,
|
||||||
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||||
|
...style,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar({
|
||||||
|
side = "left",
|
||||||
|
variant = "sidebar",
|
||||||
|
collapsible = "offcanvas",
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
side?: "left" | "right"
|
||||||
|
variant?: "sidebar" | "floating" | "inset"
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none"
|
||||||
|
}) {
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||||
|
|
||||||
|
if (collapsible === "none") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar"
|
||||||
|
className={cn(
|
||||||
|
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-slot="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
side={side}
|
||||||
|
>
|
||||||
|
<SheetHeader className="sr-only">
|
||||||
|
<SheetTitle>Sidebar</SheetTitle>
|
||||||
|
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="group peer text-sidebar-foreground hidden md:block"
|
||||||
|
data-state={state}
|
||||||
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
|
data-variant={variant}
|
||||||
|
data-side={side}
|
||||||
|
data-slot="sidebar"
|
||||||
|
>
|
||||||
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-gap"
|
||||||
|
className={cn(
|
||||||
|
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||||
|
"group-data-[collapsible=offcanvas]:w-0",
|
||||||
|
"group-data-[side=right]:rotate-180",
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||||
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-container"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||||
|
side === "left"
|
||||||
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||||
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-slot="sidebar-inner"
|
||||||
|
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarTrigger({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-sidebar="trigger"
|
||||||
|
data-slot="sidebar-trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn("size-7", className)}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event)
|
||||||
|
toggleSidebar()
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PanelLeftIcon />
|
||||||
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-sidebar="rail"
|
||||||
|
data-slot="sidebar-rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
className={cn(
|
||||||
|
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||||
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||||
|
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||||
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
data-slot="sidebar-inset"
|
||||||
|
className={cn(
|
||||||
|
"bg-background relative flex w-full flex-1 flex-col",
|
||||||
|
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Input>) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="sidebar-input"
|
||||||
|
data-sidebar="input"
|
||||||
|
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-header"
|
||||||
|
data-sidebar="header"
|
||||||
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-footer"
|
||||||
|
data-sidebar="footer"
|
||||||
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
data-slot="sidebar-separator"
|
||||||
|
data-sidebar="separator"
|
||||||
|
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-content"
|
||||||
|
data-sidebar="content"
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group"
|
||||||
|
data-sidebar="group"
|
||||||
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupLabel({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "div"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-group-label"
|
||||||
|
data-sidebar="group-label"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupAction({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-group-action"
|
||||||
|
data-sidebar="group-action"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 md:after:hidden",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group-content"
|
||||||
|
data-sidebar="group-content"
|
||||||
|
className={cn("w-full text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu"
|
||||||
|
data-sidebar="menu"
|
||||||
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-item"
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
className={cn("group/menu-item relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarMenuButtonVariants = cva(
|
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 text-sm",
|
||||||
|
sm: "h-7 text-xs",
|
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function SidebarMenuButton({
|
||||||
|
asChild = false,
|
||||||
|
isActive = false,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
isActive?: boolean
|
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
const { isMobile, state } = useSidebar()
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-button"
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tooltip === "string") {
|
||||||
|
tooltip = {
|
||||||
|
children: tooltip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="right"
|
||||||
|
align="center"
|
||||||
|
hidden={state !== "collapsed" || isMobile}
|
||||||
|
{...tooltip}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuAction({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
showOnHover = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
showOnHover?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-action"
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 md:after:hidden",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
showOnHover &&
|
||||||
|
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuBadge({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-badge"
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||||
|
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSkeleton({
|
||||||
|
className,
|
||||||
|
showIcon = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showIcon?: boolean
|
||||||
|
}) {
|
||||||
|
// Random width between 50 to 90%.
|
||||||
|
const width = React.useMemo(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-skeleton"
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showIcon && (
|
||||||
|
<Skeleton
|
||||||
|
className="size-4 rounded-md"
|
||||||
|
data-sidebar="menu-skeleton-icon"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--skeleton-width": width,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu-sub"
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
className={cn(
|
||||||
|
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSubItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-sub-item"
|
||||||
|
data-sidebar="menu-sub-item"
|
||||||
|
className={cn("group/menu-sub-item relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSubButton({
|
||||||
|
asChild = false,
|
||||||
|
size = "md",
|
||||||
|
isActive = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
size?: "sm" | "md"
|
||||||
|
isActive?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-sub-button"
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||||
|
size === "sm" && "text-xs",
|
||||||
|
size === "md" && "text-sm",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupAction,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarInput,
|
||||||
|
SidebarInset,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuBadge,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSkeleton,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarRail,
|
||||||
|
SidebarSeparator,
|
||||||
|
SidebarTrigger,
|
||||||
|
useSidebar,
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import {cn} from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Skeleton({
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
return (
|
||||||
...props
|
<div
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
data-slot="skeleton"
|
||||||
return (
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
<div
|
{...props}
|
||||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Skeleton}
|
export { Skeleton }
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue