initial
start
This commit is contained in:
parent
8a1a954603
commit
365a89ac63
47 changed files with 4970 additions and 193 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -3,8 +3,12 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
|
@ -25,9 +29,8 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
|||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 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
Normal file
14
.idea/discord.xml
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<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
Normal file
13
.idea/material_theme_project_new.xml
generated
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?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
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?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
Normal file
12
.idea/sipher.iml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?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
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
103
README.md
103
README.md
|
|
@ -1,96 +1,33 @@
|
|||
<a href="https://demo-nextjs-with-supabase.vercel.app/">
|
||||
<img alt="Next.js and Supabase Starter Kit - the fastest way to build apps with Next.js and Supabase" src="https://demo-nextjs-with-supabase.vercel.app/opengraph-image.png">
|
||||
<h1 align="center">Next.js and Supabase Starter Kit</h1>
|
||||
</a>
|
||||
- 1 - What will your software do?
|
||||
|
||||
<p align="center">
|
||||
The fastest way to build apps with Next.js and Supabase
|
||||
</p>
|
||||
My software will encrypt messages just like WhatsApp does by using a system of people having a key and sharing them
|
||||
with one another.
|
||||
|
||||
<p align="center">
|
||||
<a href="#features"><strong>Features</strong></a> ·
|
||||
<a href="#demo"><strong>Demo</strong></a> ·
|
||||
<a href="#deploy-to-vercel"><strong>Deploy to Vercel</strong></a> ·
|
||||
<a href="#clone-and-run-locally"><strong>Clone and run locally</strong></a> ·
|
||||
<a href="#feedback-and-issues"><strong>Feedback and issues</strong></a>
|
||||
<a href="#more-supabase-examples"><strong>More Examples</strong></a>
|
||||
</p>
|
||||
<br/>
|
||||
- 1.1 - What features will it have?
|
||||
|
||||
## Features
|
||||
I'll let the user choose multiple encryption methods, this will make it more secure and reliable.
|
||||
Only the user will have its password that he could share with another user.
|
||||
|
||||
- Works across the entire [Next.js](https://nextjs.org) stack
|
||||
- App Router
|
||||
- Pages Router
|
||||
- Middleware
|
||||
- Client
|
||||
- Server
|
||||
- It just works!
|
||||
- supabase-ssr. A package to configure Supabase Auth to use cookies
|
||||
- Styling with [Tailwind CSS](https://tailwindcss.com)
|
||||
- Components with [shadcn/ui](https://ui.shadcn.com/)
|
||||
- Optional deployment with [Supabase Vercel Integration and Vercel deploy](#deploy-your-own)
|
||||
- Environment variables automatically assigned to Vercel project
|
||||
- 1.2 How will it be executed?
|
||||
|
||||
## Demo
|
||||
Mainly by creating a database that would only hold a username and a password, could use Supabase for that
|
||||
or a simple MongoDb cluster.
|
||||
|
||||
You can view a fully working demo at [demo-nextjs-with-supabase.vercel.app](https://demo-nextjs-with-supabase.vercel.app/).
|
||||
- 2- What new skills will you need to acquire?
|
||||
|
||||
## Deploy to Vercel
|
||||
For this one, mainly how cryptography works on message exchanging.
|
||||
|
||||
Vercel deployment will guide you through creating a Supabase account and project.
|
||||
- 2.1 - What topics will you need to research?
|
||||
|
||||
After installation of the Supabase integration, all relevant environment variables will be assigned to the project so the deployment is fully functioning.
|
||||
I'll also have to research about the recommended cases on how to store or handle each user.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This+starter+configures+Supabase+Auth+to+use+cookies%2C+making+the+user%27s+session+available+throughout+the+entire+Next.js+app+-+Client+Components%2C+Server+Components%2C+Route+Handlers%2C+Server+Actions+and+Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png)
|
||||
- 3- If working with one or two classmates, who will do what?
|
||||
|
||||
The above will also clone the Starter kit to your GitHub, you can clone that locally and develop locally.
|
||||
Will do by myself.
|
||||
|
||||
If you wish to just develop locally and not deploy to Vercel, [follow the steps below](#clone-and-run-locally).
|
||||
- 4 - In the world of software, most everything takes longer to implement than you expect. And so it’s not uncommon to accomplish less in a fixed amount of time than you hope. What might you consider to be a good outcome for your project? A better outcome? The best outcome?
|
||||
|
||||
## Clone and run locally
|
||||
|
||||
1. You'll first need a Supabase project which can be made [via the Supabase dashboard](https://database.new)
|
||||
|
||||
2. Create a Next.js app using the Supabase Starter template npx command
|
||||
|
||||
```bash
|
||||
npx create-next-app -e with-supabase
|
||||
```
|
||||
|
||||
3. Use `cd` to change into the app's directory
|
||||
|
||||
```bash
|
||||
cd name-of-new-app
|
||||
```
|
||||
|
||||
4. Rename `.env.example` to `.env.local` and update the following:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL=[INSERT SUPABASE PROJECT URL]
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=[INSERT SUPABASE PROJECT API ANON KEY]
|
||||
```
|
||||
|
||||
Both `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api)
|
||||
|
||||
5. You can now run the Next.js local development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The starter kit should now be running on [localhost:3000](http://localhost:3000/).
|
||||
|
||||
6. This template comes with the default shadcn/ui style initialized. If you instead want other ui.shadcn styles, delete `components.json` and [re-install shadcn/ui](https://ui.shadcn.com/docs/installation/next)
|
||||
|
||||
> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally.
|
||||
|
||||
## Feedback and issues
|
||||
|
||||
Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose).
|
||||
|
||||
## More Supabase examples
|
||||
|
||||
- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments)
|
||||
- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF)
|
||||
- [Supabase Auth and the Next.js App Router](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs)
|
||||
The best outcome for this would be an app that could at least:
|
||||
Log in/Register the user
|
||||
Let the user choose its encryption method
|
||||
Let the user change his password to a maximum of a 12-letter word
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
2804
package-lock.json
generated
Normal file
2804
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
51
package.json
51
package.json
|
|
@ -1,36 +1,41 @@
|
|||
{
|
||||
"name": "whispr",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@supabase/ssr": "latest",
|
||||
"@supabase/supabase-js": "latest",
|
||||
"autoprefixer": "10.4.20",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.47.3",
|
||||
"argon2": "^0.41.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.2.1",
|
||||
"lucide-react": "^0.456.0",
|
||||
"next": "latest",
|
||||
"next-themes": "^0.4.3",
|
||||
"prettier": "^3.3.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"framer-motion": "^11.13.5",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "15.0.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.9.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"postcss": "8.4.49",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "3.4.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.6.3"
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/logos/logo-light.png
Normal file
BIN
public/logos/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
public/logos/logo.png
Normal file
BIN
public/logos/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
public/logos/united-chat.png
Normal file
BIN
public/logos/united-chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6 KiB |
44
src/app/api/auth/get_user/route.ts
Normal file
44
src/app/api/auth/get_user/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import {createClient} from "@/lib/supabase/server";
|
||||
import {NextResponse} from "next/server";
|
||||
|
||||
// Helper function to get user data by UUID
|
||||
async function getUserByUUID(supabase: any, uuid: string) {
|
||||
const {data: userData, error: userError} = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('uuid', uuid)
|
||||
.single();
|
||||
|
||||
if (userError) throw userError;
|
||||
return userData;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const {searchParams} = new URL(request.url);
|
||||
const uuid = searchParams.get('uuid');
|
||||
|
||||
if (uuid) {
|
||||
// Get specific user by UUID
|
||||
const userData = await getUserByUUID(supabase, uuid);
|
||||
return NextResponse.json({user: userData});
|
||||
} 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) {
|
||||
return NextResponse.json(
|
||||
{error: `Failed to fetch user: ${error}`},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/app/api/auth/login/route.ts
Normal file
38
src/app/api/auth/login/route.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// 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()
|
||||
|
||||
// Mocks the email with the domain we configured on the local env
|
||||
const email = `${username.toLowerCase()}@${process.env.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('*')
|
||||
.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}
|
||||
)
|
||||
}
|
||||
}
|
||||
45
src/app/api/auth/register/route.ts
Normal file
45
src/app/api/auth/register/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {NextResponse} from 'next/server'
|
||||
import {createClient} from "@/lib/supabase/server";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const {username, password} = await request.json()
|
||||
const supabase = await createClient()
|
||||
|
||||
try {
|
||||
// First create the auth user
|
||||
const {data: {user}, error: authError} = await supabase.auth.signUp({
|
||||
email: `${username}@${process.env.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,
|
||||
})
|
||||
|
||||
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: `Registration failed: ${JSON.stringify(error)}`},
|
||||
{status: 400}
|
||||
)
|
||||
}
|
||||
return NextResponse.json(
|
||||
{error: `Registration failed: ${error}`},
|
||||
{status: 400}
|
||||
)
|
||||
}
|
||||
}
|
||||
37
src/app/api/user/get/threads/route.ts
Normal file
37
src/app/api/user/get/threads/route.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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 (data.length === 0) {
|
||||
return NextResponse.json({threads: []}, {status: 200});
|
||||
}
|
||||
|
||||
return NextResponse.json({threads: data}, {status: 200});
|
||||
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
36
src/app/auth/login/login.ts
Normal file
36
src/app/auth/login/login.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
*
|
||||
* @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"};
|
||||
}
|
||||
}
|
||||
198
src/app/auth/login/page.tsx
Normal file
198
src/app/auth/login/page.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"use client"
|
||||
|
||||
import React, {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();
|
||||
|
||||
useEffect(() => {
|
||||
const check = async () => {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (isAuthenticated) {
|
||||
router.replace('/');
|
||||
} else {
|
||||
setMounted(true);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
}, [checkAuth, router]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
|
||||
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) {
|
||||
if (isLogin && response.code === 400) {
|
||||
console.log(response)
|
||||
toast({
|
||||
title: "E-mail not verified",
|
||||
description: response.message,
|
||||
variant: "destructive",
|
||||
duration: 5000, // Increased duration for better visibility
|
||||
action: response.action!
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
description: response.message,
|
||||
variant: "destructive",
|
||||
duration: 5000, // Increased duration for better visibility
|
||||
});
|
||||
} 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>
|
||||
)
|
||||
}
|
||||
38
src/app/auth/login/register.ts
Normal file
38
src/app/auth/login/register.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
*
|
||||
* @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(password: string, username: string) {
|
||||
try {
|
||||
// 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}), // 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: data.code,
|
||||
message: data.message
|
||||
}
|
||||
}
|
||||
|
||||
// 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: `An unknown error occurred: ${e.message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/app/globals.css
Normal file
61
src/app/globals.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--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%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
75
src/app/layout.tsx
Normal file
75
src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// app/layout.tsx
|
||||
import type {Metadata} from "next";
|
||||
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 "next-themes";
|
||||
|
||||
const publicSans = Public_Sans({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-public-sans'
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode & { props?: { childProp?: { segment?: string } } };
|
||||
}) {
|
||||
const initialUser = await getAuthenticatedUser();
|
||||
const isAuthPage = (children as any)?.props?.childProp?.segment === 'auth';
|
||||
|
||||
// Auth layout
|
||||
if (isAuthPage) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${publicSans.variable} font-sans antialiased`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
>
|
||||
<UserProvider initialUser={initialUser}>
|
||||
<main className="min-h-screen flex items-center justify-center">
|
||||
{children}
|
||||
</main>
|
||||
</UserProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
// Main layout
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${publicSans.variable} font-sans antialiased`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
>
|
||||
<UserProvider initialUser={initialUser}>
|
||||
<SharedStateProvider>
|
||||
<div className={`max-h-[1080px] p-6 bg-secondary`}>
|
||||
<div className="flex bg-background">
|
||||
<Sidebar>
|
||||
{children}
|
||||
</Sidebar>
|
||||
</div>
|
||||
</div>
|
||||
</SharedStateProvider>
|
||||
</UserProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
12
src/app/page.tsx
Normal file
12
src/app/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"use client"
|
||||
import {useTheme} from "next-themes";
|
||||
|
||||
export default function SiPher() {
|
||||
const {theme} = useTheme()
|
||||
|
||||
return (
|
||||
<div className={`flex-1 ${theme === "dark" ? "dark" : ""}`}>
|
||||
abc
|
||||
</div>
|
||||
)
|
||||
}
|
||||
58
src/components/main/sidebar/mobile.tsx
Normal file
58
src/components/main/sidebar/mobile.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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
|
||||
229
src/components/main/sidebar/sidebar.tsx
Normal file
229
src/components/main/sidebar/sidebar.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"use client"
|
||||
import React, {useCallback, useEffect, useRef, useState} from "react"
|
||||
import {usePathname} from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import {AnimatePresence, motion} from "framer-motion"
|
||||
import {LogOut, X} from "lucide-react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Avatar, AvatarFallback} from "@/components/ui/avatar"
|
||||
import {Separator} from "@/components/ui/separator"
|
||||
import {ScrollArea} from "@/components/ui/scroll-area"
|
||||
import {useTheme} from "next-themes"
|
||||
import {GearIcon} from "@radix-ui/react-icons"
|
||||
import Image from "next/image";
|
||||
import MobileHeader from "@/components/main/sidebar/mobile";
|
||||
import {useUser} from "@/contexts/user";
|
||||
import {useUIState} from "@/hooks/shared-states";
|
||||
import {useToast} from "@/hooks/use-toast";
|
||||
|
||||
type SidebarProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function Sidebar(
|
||||
{
|
||||
children
|
||||
}: SidebarProps
|
||||
) {
|
||||
const pathname = usePathname()
|
||||
const drawerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [selectedThreads, setSelectedThreads] = useState("");
|
||||
const [threads, setThreads] = useState<SiPher.Messages[] | []>([]);
|
||||
const [threadMenu, setThreadMenu] = useState<SiPher.Messages[] | []>([]);
|
||||
const {toast} = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const getThreads = async () => {
|
||||
const req = await fetch("/api/user/get/threads")
|
||||
|
||||
if (req.ok) {
|
||||
const {threads} = await req.json() as { threads: SiPher.Messages[] | [] }
|
||||
setThreads(threads)
|
||||
return;
|
||||
} else {
|
||||
setThreads([]);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "An unknown error occurred",
|
||||
variant: "destructive",
|
||||
duration: 5000, // Increased duration for better visibility
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getThreads();
|
||||
|
||||
return () => {
|
||||
setThreads([]);
|
||||
}
|
||||
}, [setThreads])
|
||||
|
||||
const generateThreads = useCallback(() => {
|
||||
threads.map(async(thread) => {
|
||||
if (thread.participants.length > 2) {
|
||||
return (
|
||||
<li key={thread.id}>
|
||||
<Link href={thread.id} passHref>
|
||||
<Button
|
||||
variant={pathname === thread.id ? "secondary" : "ghost"}
|
||||
className="w-full justify-start text-[17px] py-4"
|
||||
>
|
||||
<Avatar className="w-8 h-8 mr-3 p-1">
|
||||
<AvatarFallback>{thread.name!}</AvatarFallback>
|
||||
</Avatar>
|
||||
{thread.name!}
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
} else {
|
||||
const fetchOtherUser = await useUser().getUser(thread.id)
|
||||
}
|
||||
})
|
||||
}, [threads])
|
||||
|
||||
const user = useUser().user!;
|
||||
|
||||
const {
|
||||
username,
|
||||
suuid
|
||||
} = user
|
||||
|
||||
const {isDrawerOpen, 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 isDarkMode = getTheme() === "dark";
|
||||
|
||||
const RightSidebarContent = () => (
|
||||
<div className={`flex flex-col h-full w-[240px]`}>
|
||||
<div
|
||||
className={`flex items-center p-3 m-2 ${isDarkMode ? "hover:bg-accent/90" : "hover:bg-secondary/20"} rounded-full transition-colors duration-200`}>
|
||||
<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-sm text-muted-foreground">@{username}</p>
|
||||
<p className="text-xs text-muted">${suuid}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-2"/>
|
||||
<ScrollArea className="flex-grow max-h-[590px] px-4 py-4">
|
||||
<nav>
|
||||
<ul className="space-y-1">
|
||||
{threads.map((thread) => (
|
||||
<li key={thread.id}>
|
||||
<Link href={thread.id} passHref>
|
||||
<Button
|
||||
variant={pathname === thread.id ? "secondary" : "ghost"}
|
||||
className="w-full justify-start text-[17px] py-4"
|
||||
>
|
||||
<Avatar className="w-8 h-8 mr-3 p-1">
|
||||
<AvatarFallback>{thread.id}</AvatarFallback>
|
||||
</Avatar>
|
||||
{thread.id}
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</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 = "/config"}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
|
||||
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">
|
||||
<Link href={"/"} passHref>
|
||||
<Image
|
||||
src={isDarkMode ? "/logos/logo.png" : "/logos/logo-light.png"}
|
||||
alt="Tocka‘s Nest"
|
||||
width={64}
|
||||
height={64}
|
||||
className="w-16 h-16 cursor-pointer rounded-full hover:bg-secondary/20"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<RightSidebarContent/>
|
||||
</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/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{
|
||||
children ?? null
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"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 }
|
||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
76
src/components/ui/card.tsx
Normal file
76
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
129
src/components/ui/toast.tsx
Normal file
129
src/components/ui/toast.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
35
src/components/ui/toaster.tsx
Normal file
35
src/components/ui/toaster.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"use client"
|
||||
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
82
src/contexts/user.tsx
Normal file
82
src/contexts/user.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// contexts/user.tsx
|
||||
'use client';
|
||||
|
||||
import {createContext, useContext} from 'react';
|
||||
import {useRouter} from 'next/navigation';
|
||||
|
||||
interface UserContextType {
|
||||
user: NonNullable<SiPher.User>;
|
||||
getUser: () => Promise<NonNullable<SiPher.User>>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextType | null>(null);
|
||||
|
||||
export function useUser() {
|
||||
const context = useContext(UserContext);
|
||||
const router = useRouter();
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useUser must be used within a UserProvider');
|
||||
}
|
||||
|
||||
return {
|
||||
user: context.user,
|
||||
getUser: async (userId?: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/auth/get_user?${
|
||||
userId && `uuid=${
|
||||
encodeURIComponent(userId)
|
||||
}`
|
||||
}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
if (error.message?.includes("Auth session missing!")) {
|
||||
throw new Error('No authenticated user');
|
||||
}
|
||||
throw new Error(error.message || 'Authentication failed');
|
||||
}
|
||||
|
||||
const {user} = await response.json();
|
||||
return user as NonNullable<SiPher.User>;
|
||||
} catch (error) {
|
||||
console.error('Failed to get user:', error);
|
||||
router.push('/auth/login');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
checkAuth: async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/get/user');
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function UserProvider(
|
||||
{
|
||||
children,
|
||||
initialUser
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialUser: NonNullable<SiPher.User>;
|
||||
}
|
||||
) {
|
||||
return (
|
||||
<UserContext.Provider value={{
|
||||
user: initialUser,
|
||||
getUser: async () => {
|
||||
const response = await fetch('/api/auth/get/user');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get user');
|
||||
}
|
||||
const {user} = await response.json();
|
||||
return user as NonNullable<SiPher.User>;
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
171
src/hooks/shared-states.tsx
Normal file
171
src/hooks/shared-states.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
// src/hooks/useSharedState.tsx
|
||||
import React, {createContext, MutableRefObject, useContext, useRef, useState} from 'react'
|
||||
import {useTheme} from 'next-themes'
|
||||
|
||||
// Define the shape of our shared state
|
||||
interface SharedState {
|
||||
// UI States
|
||||
isScrolled: boolean
|
||||
setIsScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isSearchExpanded: boolean
|
||||
setIsSearchExpanded: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isDrawerOpen: boolean
|
||||
setIsDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isCreateModalOpen: boolean
|
||||
setIsCreateModalOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isUserModalOpen: boolean
|
||||
setIsUserModalOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isNotificationsOpen: boolean
|
||||
setIsNotificationsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
showBackToTop: boolean
|
||||
setShowBackToTop: React.Dispatch<React.SetStateAction<boolean>>
|
||||
|
||||
// Refs
|
||||
drawerRef: React.RefObject<HTMLDivElement>
|
||||
userModalRef: React.RefObject<HTMLDivElement>
|
||||
notificationsRef: React.RefObject<HTMLDivElement>
|
||||
createModalRef: React.RefObject<HTMLDivElement>
|
||||
fileInputRef: React.RefObject<HTMLInputElement>
|
||||
observerRef: MutableRefObject<IntersectionObserver | null>
|
||||
loadingRef: MutableRefObject<boolean>
|
||||
|
||||
// Theme
|
||||
theme: string | undefined
|
||||
}
|
||||
|
||||
export function useMutableRef<T>(initialValue: T): MutableRefObject<T> {
|
||||
return useRef<T>(initialValue) as MutableRefObject<T>
|
||||
}
|
||||
|
||||
// Create the context
|
||||
const SharedStateContext = createContext<SharedState | undefined>(undefined)
|
||||
|
||||
// Create the provider component
|
||||
export function SharedStateProvider({children}: { children: React.ReactNode }) {
|
||||
// UI States
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
const [isSearchExpanded, setIsSearchExpanded] = useState(false)
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
const [isUserModalOpen, setIsUserModalOpen] = useState(false)
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false)
|
||||
const [showBackToTop, setShowBackToTop] = useState(false)
|
||||
|
||||
// Refs
|
||||
const drawerRef = useRef<HTMLDivElement>(null)
|
||||
const userModalRef = useRef<HTMLDivElement>(null)
|
||||
const notificationsRef = useRef<HTMLDivElement>(null)
|
||||
const createModalRef = useRef<HTMLDivElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const loadingRef = useMutableRef<boolean>(false)
|
||||
const observerRef = useMutableRef<IntersectionObserver | null>(null)
|
||||
|
||||
// Theme
|
||||
const {theme} = useTheme()
|
||||
|
||||
const value = {
|
||||
// UI States
|
||||
isScrolled,
|
||||
setIsScrolled,
|
||||
isSearchExpanded,
|
||||
setIsSearchExpanded,
|
||||
isDrawerOpen,
|
||||
setIsDrawerOpen,
|
||||
isCreateModalOpen,
|
||||
setIsCreateModalOpen,
|
||||
isUserModalOpen,
|
||||
setIsUserModalOpen,
|
||||
isNotificationsOpen,
|
||||
setIsNotificationsOpen,
|
||||
showBackToTop,
|
||||
setShowBackToTop,
|
||||
|
||||
// Refs
|
||||
drawerRef,
|
||||
userModalRef,
|
||||
notificationsRef,
|
||||
createModalRef,
|
||||
fileInputRef,
|
||||
observerRef,
|
||||
loadingRef,
|
||||
|
||||
// Theme
|
||||
theme,
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedStateContext.Provider value={value}>
|
||||
{children}
|
||||
</SharedStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Create the custom hook
|
||||
export function useSharedState() {
|
||||
const context = useContext(SharedStateContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useSharedState must be used within a SharedStateProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Optional: Create specific hooks for different parts of the state
|
||||
export function useUIState() {
|
||||
const {
|
||||
isScrolled,
|
||||
setIsScrolled,
|
||||
isSearchExpanded,
|
||||
setIsSearchExpanded,
|
||||
isDrawerOpen,
|
||||
setIsDrawerOpen,
|
||||
isCreateModalOpen,
|
||||
setIsCreateModalOpen,
|
||||
isUserModalOpen,
|
||||
setIsUserModalOpen,
|
||||
isNotificationsOpen,
|
||||
setIsNotificationsOpen,
|
||||
showBackToTop,
|
||||
setShowBackToTop,
|
||||
} = useSharedState()
|
||||
|
||||
return {
|
||||
isScrolled,
|
||||
setIsScrolled,
|
||||
isSearchExpanded,
|
||||
setIsSearchExpanded,
|
||||
isDrawerOpen,
|
||||
setIsDrawerOpen,
|
||||
isCreateModalOpen,
|
||||
setIsCreateModalOpen,
|
||||
isUserModalOpen,
|
||||
setIsUserModalOpen,
|
||||
isNotificationsOpen,
|
||||
setIsNotificationsOpen,
|
||||
showBackToTop,
|
||||
setShowBackToTop,
|
||||
}
|
||||
}
|
||||
|
||||
export function useRefs() {
|
||||
const {
|
||||
drawerRef,
|
||||
userModalRef,
|
||||
notificationsRef,
|
||||
createModalRef,
|
||||
fileInputRef,
|
||||
observerRef,
|
||||
loadingRef,
|
||||
} = useSharedState()
|
||||
|
||||
return {
|
||||
drawerRef,
|
||||
userModalRef,
|
||||
notificationsRef,
|
||||
createModalRef,
|
||||
fileInputRef,
|
||||
observerRef,
|
||||
loadingRef,
|
||||
}
|
||||
}
|
||||
194
src/hooks/use-toast.ts
Normal file
194
src/hooks/use-toast.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
41
src/lib/auth/index.ts
Normal file
41
src/lib/auth/index.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// lib/auth/index.ts
|
||||
import {createClient} from '@/lib/supabase/server';
|
||||
import {headers} from 'next/headers';
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mostly used for getting the first user to prevent it being null
|
||||
*/
|
||||
export async function getAuthenticatedUser() {
|
||||
const headersList = await headers();
|
||||
const path = headersList.get("x-invoke-path") || "";
|
||||
|
||||
// If we're on a public path, don't require authentication
|
||||
if (PUBLIC_PATHS.some(publicPath => path.startsWith(publicPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
const {data: {user: session}, error: sessionError} = await supabase.auth.getUser();
|
||||
|
||||
if (sessionError || !session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {data: profile, error: profileError} = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('uuid', session.id)
|
||||
.single();
|
||||
|
||||
if (profileError || !profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
38
src/lib/supabase/server.ts
Normal file
38
src/lib/supabase/server.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import {CookieOptions, createServerClient} from '@supabase/ssr';
|
||||
|
||||
import {cookies} from 'next/headers';
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll().map(cookie => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
}))
|
||||
},
|
||||
setAll(cookiesList: { name: string; value: string; options?: CookieOptions }[]) {
|
||||
try {
|
||||
cookiesList.forEach(({name, value, options}) => {
|
||||
cookieStore.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
// Ensure cookies are secure in production
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error setting cookies:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
59
src/middleware.ts
Normal file
59
src/middleware.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import {NextRequest, NextResponse} from "next/server";
|
||||
import {createClient} from "@/lib/supabase/server";
|
||||
|
||||
const PUBLIC_ROUTES = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/api/auth',
|
||||
'/_next',
|
||||
'/favicon.ico',
|
||||
'/static',
|
||||
'/images',
|
||||
];
|
||||
|
||||
const isPublicRoute = (path: string) => {
|
||||
return PUBLIC_ROUTES.some(route => path.startsWith(route));
|
||||
}
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const {data: {user}, error} = await supabase.auth.getUser();
|
||||
const path = request.nextUrl.pathname;
|
||||
|
||||
if (!user && !isPublicRoute(path)) {
|
||||
const redirectUrl = new URL('/auth/login', request.url);
|
||||
if (request.nextUrl.search) {
|
||||
redirectUrl.search = request.nextUrl.search;
|
||||
}
|
||||
redirectUrl.searchParams.set('redirectTo', request.nextUrl.pathname);
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
if (user && path.startsWith('/auth/') && !path.includes("/auth/complete")) {
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
}
|
||||
|
||||
if (user?.id) {
|
||||
response.headers.set('x-user-id', user.id);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Middleware error:', error);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
"/api/preferences/language",
|
||||
],
|
||||
}
|
||||
31
src/types/user.d.ts
vendored
Normal file
31
src/types/user.d.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
declare global {
|
||||
namespace SiPher {
|
||||
type Messages = {
|
||||
id: string;
|
||||
participants: string[];
|
||||
name?: string;
|
||||
messages: {
|
||||
id: string;
|
||||
content: string;
|
||||
}[];
|
||||
indexable?: boolean;
|
||||
}
|
||||
|
||||
type User = {
|
||||
/** Represents the unique username of a user. */
|
||||
username: string,
|
||||
/** The encrypted password of said user. */
|
||||
password: string,
|
||||
/** Unique UUID, long */
|
||||
uuid: string,
|
||||
/** Short UUID, for index reasons */
|
||||
suuid: string,
|
||||
/** Created at timestamp in UTC */
|
||||
created_at: string,
|
||||
/** Messages field */
|
||||
messages: Messages[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
@ -1,80 +1,62 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
|
@ -20,7 +19,7 @@
|
|||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue