Compare commits
15 Commits
dev1
...
2c35b781dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c35b781dd | ||
|
|
31e8e355f3 | ||
|
|
23945b3487 | ||
|
|
b4f98fe6cd | ||
|
|
1f351bc32b | ||
|
|
3708a612f7 | ||
|
|
4fe2d39fba | ||
|
|
15426adc69 | ||
|
|
eee26d06b7 | ||
|
|
e0f22b6c5a | ||
|
|
4a33b8defa | ||
|
|
7978b1042d | ||
|
|
33bd88629f | ||
|
|
e41b8922e5 | ||
|
|
4211be86ff |
@@ -42,11 +42,17 @@ class RefreshToken(BaseModel):
|
|||||||
expire_days: int
|
expire_days: int
|
||||||
|
|
||||||
|
|
||||||
|
class CorsSettings(BaseModel):
|
||||||
|
production: str
|
||||||
|
local: list[str] = ["http://localhost:5173", "http://localhost:5174"]
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
api: ApiPrefix = ApiPrefix()
|
api: ApiPrefix = ApiPrefix()
|
||||||
db: DbSettings = DbSettings()
|
db: DbSettings = DbSettings()
|
||||||
access_token: AccessToken
|
access_token: AccessToken
|
||||||
refresh_token: RefreshToken
|
refresh_token: RefreshToken
|
||||||
|
cors_settings: CorsSettings
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env", env_file_encoding="utf-8", env_nested_delimiter="__"
|
env_file=".env", env_file_encoding="utf-8", env_nested_delimiter="__"
|
||||||
)
|
)
|
||||||
|
|||||||
11
src/main.py
11
src/main.py
@@ -5,10 +5,21 @@ import uvicorn
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).parent.parent))
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from src.api import router
|
from src.api import router
|
||||||
|
from src.core.settings import settings
|
||||||
|
|
||||||
app = FastAPI(title="Task&Coffee")
|
app = FastAPI(title="Task&Coffee")
|
||||||
app.include_router(router=router)
|
app.include_router(router=router)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.cors_settings.local + [settings.cors_settings.production],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("src.main:app", port=8000, log_level="info", reload=True)
|
uvicorn.run("src.main:app", port=8000, log_level="info", reload=True)
|
||||||
|
|||||||
24
taskncoffee-app/.gitignore
vendored
Normal file
24
taskncoffee-app/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
16
taskncoffee-app/README.md
Normal file
16
taskncoffee-app/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||||
22
taskncoffee-app/components.json
Normal file
22
taskncoffee-app/components.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": false,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "gray",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
39
taskncoffee-app/eslint.config.js
Normal file
39
taskncoffee-app/eslint.config.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
// js.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"react/jsx-uses-react": "error",
|
||||||
|
|
||||||
|
'no-unused-vars': ['error', {
|
||||||
|
varsIgnorePattern: '^[A-Z_]',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}],
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
taskncoffee-app/index.html
Normal file
13
taskncoffee-app/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>taskncoffee-app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
taskncoffee-app/jsconfig.json
Normal file
8
taskncoffee-app/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5776
taskncoffee-app/package-lock.json
generated
Normal file
5776
taskncoffee-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
taskncoffee-app/package.json
Normal file
43
taskncoffee-app/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "taskncoffee-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
|
"motion": "^12.23.22",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router": "^7.9.4",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.1.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@types/react": "^19.1.16",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
taskncoffee-app/src/App.css
Normal file
0
taskncoffee-app/src/App.css
Normal file
32
taskncoffee-app/src/App.jsx
Normal file
32
taskncoffee-app/src/App.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import './App.css'
|
||||||
|
import { LoginPage } from './pages/Login'
|
||||||
|
import { SignUp } from './pages/SignUp'
|
||||||
|
import { AuthLayout } from './layouts/AuthLayout'
|
||||||
|
import { Routes, Route, Navigate } from 'react-router'
|
||||||
|
import MenuDockVertical from './layouts/DashLayout'
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/auth/login" replace />} />
|
||||||
|
|
||||||
|
<Route path="/auth" element={
|
||||||
|
<AuthLayout className="flex min-h-svh flex-col items-center justify-center bg-muted text-foreground" />
|
||||||
|
}>
|
||||||
|
<Route path="login" element={<LoginPage />} />
|
||||||
|
<Route path="signup" element={<SignUp />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/dashboard" element={
|
||||||
|
// <div className="min-h-svh bg-background flex justify-start items-start">
|
||||||
|
<MenuDockVertical />
|
||||||
|
// </div>
|
||||||
|
} />
|
||||||
|
<Route path="*" element={<Navigate to="/auth/login" replace />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
37
taskncoffee-app/src/apiv1/ApiSources.jsx
Normal file
37
taskncoffee-app/src/apiv1/ApiSources.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
const API_V1_PREFIX = '/api/v1';
|
||||||
|
|
||||||
|
const API_SOURCES = {
|
||||||
|
AUTH: `${API_BASE_URL}${API_V1_PREFIX}/auth`,
|
||||||
|
USERS: `${API_BASE_URL}${API_V1_PREFIX}/users`,
|
||||||
|
TASKS: `${API_BASE_URL}${API_V1_PREFIX}/tasks`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_ENDPOINTS = {
|
||||||
|
AUTH: {
|
||||||
|
LOGIN: `${API_SOURCES.AUTH}/login`,
|
||||||
|
REFRESH: `${API_SOURCES.AUTH}/refresh`,
|
||||||
|
LOGOUT: `${API_SOURCES.AUTH}/logout`,
|
||||||
|
ME: `${API_SOURCES.AUTH}/me`,
|
||||||
|
REGISTER: `${API_SOURCES.AUTH}/signup`,
|
||||||
|
},
|
||||||
|
|
||||||
|
USERS: {
|
||||||
|
LIST: API_SOURCES.USERS,
|
||||||
|
BY_ID: (id) => `${API_SOURCES.USERS}/${id}`,
|
||||||
|
TASKS: (id) => `${API_SOURCES.USERS}/${id}/tasks`,
|
||||||
|
UPDATE: (id) => `${API_SOURCES.USERS}/${id}`,
|
||||||
|
DELETE: (id) => `${API_SOURCES.USERS}/${id}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
TASKS: {
|
||||||
|
BY_ID: (id) => `${API_SOURCES.TASKS}/${id}`,
|
||||||
|
CREATE: `${API_SOURCES.TASKS}`,
|
||||||
|
UPDATE: (id) => `${API_SOURCES.TASKS}/${id}`,
|
||||||
|
DELETE: (id) => `${API_SOURCES.TASKS}/${id}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export { API_BASE_URL, API_V1_PREFIX, API_SOURCES, API_ENDPOINTS };
|
||||||
83
taskncoffee-app/src/apiv1/auth.service.js
Normal file
83
taskncoffee-app/src/apiv1/auth.service.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import client from './client';
|
||||||
|
import { API_ENDPOINTS } from './ApiSources';
|
||||||
|
import { getFingerprint } from './fingerprint';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New user registration
|
||||||
|
* @param {Object} userData - User data
|
||||||
|
* @param {string} userData.username - Username
|
||||||
|
* @param {string} userData.password - Password
|
||||||
|
* @param {string} userData.email - Email (optional)
|
||||||
|
* @returns {Promise<Object>} Registered user data
|
||||||
|
*/
|
||||||
|
export const signup = async (userData) => {
|
||||||
|
try {
|
||||||
|
const response = await client.post(API_ENDPOINTS.AUTH.REGISTER, userData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response?.data || error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user information
|
||||||
|
* @returns {Promise<Object>} Current user information
|
||||||
|
*/
|
||||||
|
export const getMe = async () => {
|
||||||
|
try {
|
||||||
|
const response = await client.get(API_ENDPOINTS.AUTH.ME);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response?.data || error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User logout
|
||||||
|
* Backend automatically removes refresh token from httpOnly cookie
|
||||||
|
* @returns {Promise<Object>} Logout result
|
||||||
|
*/
|
||||||
|
export const logout = async () => {
|
||||||
|
try {
|
||||||
|
const response = await client.post(API_ENDPOINTS.AUTH.LOGOUT);
|
||||||
|
if (response.data.status_code === 200) {
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('fingerprint');
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('fingerprint');
|
||||||
|
throw error.response?.data || error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User login
|
||||||
|
* @param {string} username - Username
|
||||||
|
* @param {string} password - Password
|
||||||
|
* @returns {Promise<Object>} Login result
|
||||||
|
*/
|
||||||
|
export const login = async (username, password) => {
|
||||||
|
try {
|
||||||
|
const fingerprint = await getFingerprint();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('username', username);
|
||||||
|
formData.append('password', password);
|
||||||
|
formData.append('fingerprint', fingerprint);
|
||||||
|
const response = await client.post(API_ENDPOINTS.AUTH.LOGIN, formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(response.data);
|
||||||
|
if (response.data.access_token && response.data.token_type === 'bearer') {
|
||||||
|
localStorage.setItem('access_token', response.data.access_token);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response?.data || error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
23
taskncoffee-app/src/apiv1/client.js
Normal file
23
taskncoffee-app/src/apiv1/client.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_BASE_URL, API_V1_PREFIX } from './ApiSources';
|
||||||
|
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: `${API_BASE_URL}${API_V1_PREFIX}`,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export default client;
|
||||||
42
taskncoffee-app/src/apiv1/fingerprint.js
Normal file
42
taskncoffee-app/src/apiv1/fingerprint.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||||
|
|
||||||
|
let fpPromise = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise} Promise с экземпляром FingerprintJS
|
||||||
|
*/
|
||||||
|
const initFingerprint = () => {
|
||||||
|
if (!fpPromise) {
|
||||||
|
fpPromise = FingerprintJS.load();
|
||||||
|
}
|
||||||
|
return fpPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<string>} Fingerprint ID
|
||||||
|
*/
|
||||||
|
export const getFingerprint = async () => {
|
||||||
|
try {
|
||||||
|
const savedFingerprint = localStorage.getItem('fingerprint');
|
||||||
|
if (savedFingerprint) {
|
||||||
|
return savedFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fp = await initFingerprint();
|
||||||
|
const result = await fp.get();
|
||||||
|
const fingerprint = result.visitorId;
|
||||||
|
|
||||||
|
localStorage.setItem('fingerprint', fingerprint);
|
||||||
|
return fingerprint;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting fingerprint:', error);
|
||||||
|
const fallbackFingerprint = `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
localStorage.setItem('fingerprint', fallbackFingerprint);
|
||||||
|
return fallbackFingerprint;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearFingerprint = () => {
|
||||||
|
localStorage.removeItem('fingerprint');
|
||||||
|
};
|
||||||
|
|
||||||
BIN
taskncoffee-app/src/assets/fonts/Carry-You.ttf
Normal file
BIN
taskncoffee-app/src/assets/fonts/Carry-You.ttf
Normal file
Binary file not shown.
BIN
taskncoffee-app/src/assets/fonts/Fabulous.ttf
Normal file
BIN
taskncoffee-app/src/assets/fonts/Fabulous.ttf
Normal file
Binary file not shown.
BIN
taskncoffee-app/src/assets/fonts/Sacramento.woff2
Normal file
BIN
taskncoffee-app/src/assets/fonts/Sacramento.woff2
Normal file
Binary file not shown.
BIN
taskncoffee-app/src/assets/fonts/vibur.woff2
Normal file
BIN
taskncoffee-app/src/assets/fonts/vibur.woff2
Normal file
Binary file not shown.
42
taskncoffee-app/src/components/buttons.jsx
Normal file
42
taskncoffee-app/src/components/buttons.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
|
export function useRipple() {
|
||||||
|
const [ripples, setRipples] = useState([])
|
||||||
|
|
||||||
|
const addRipple = (e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
const id = Date.now()
|
||||||
|
|
||||||
|
setRipples((prev) => [...prev, { x, y, id }])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setRipples((prev) => prev.filter((r) => r.id !== id))
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Ripples = (
|
||||||
|
<AnimatePresence>
|
||||||
|
{ripples.map((ripple) => (
|
||||||
|
<motion.span
|
||||||
|
key={ripple.id}
|
||||||
|
initial={{ scale: 0, opacity: 0.6 }}
|
||||||
|
animate={{ scale: 4, opacity: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
|
style={{
|
||||||
|
left: ripple.x,
|
||||||
|
top: ripple.y,
|
||||||
|
}}
|
||||||
|
className="absolute h-8 w-8 rounded-full bg-white/70 pointer-events-none -translate-x-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { addRipple, Ripples }
|
||||||
|
}
|
||||||
46
taskncoffee-app/src/components/test.jsx
Normal file
46
taskncoffee-app/src/components/test.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { useState } from "react"
|
||||||
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
|
export default function RippleButton({ children }) {
|
||||||
|
const [ripples, setRipples] = useState([])
|
||||||
|
|
||||||
|
const addRipple = (e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
const id = Date.now()
|
||||||
|
|
||||||
|
setRipples([...ripples, { x, y, id }])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setRipples((r) => r.filter((ripple) => ripple.id !== id))
|
||||||
|
}, 600) // ripple длится ~600ms
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={addRipple}
|
||||||
|
className="relative overflow-hidden rounded-lg bg-blue-600 px-4 py-2 text-white font-medium"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<AnimatePresence>
|
||||||
|
{ripples.map((ripple) => (
|
||||||
|
<motion.span
|
||||||
|
key={ripple.id}
|
||||||
|
initial={{ scale: 0, opacity: 0.6 }}
|
||||||
|
animate={{ scale: 4, opacity: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
|
style={{
|
||||||
|
left: ripple.x,
|
||||||
|
top: ripple.y,
|
||||||
|
}}
|
||||||
|
className="absolute h-8 w-8 rounded-full bg-white/70 pointer-events-none -translate-x-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
taskncoffee-app/src/components/ui/avatar.jsx
Normal file
45
taskncoffee-app/src/components/ui/avatar.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
56
taskncoffee-app/src/components/ui/button.jsx
Normal file
56
taskncoffee-app/src/components/ui/button.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
101
taskncoffee-app/src/components/ui/card.jsx
Normal file
101
taskncoffee-app/src/components/ui/card.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@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
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (<div data-slot="card-content" className={cn("px-6", className)} {...props} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
24
taskncoffee-app/src/components/ui/input.jsx
Normal file
24
taskncoffee-app/src/components/ui/input.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
21
taskncoffee-app/src/components/ui/label.jsx
Normal file
21
taskncoffee-app/src/components/ui/label.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
185
taskncoffee-app/src/components/ui/shadcn-io/menu-dock/index.jsx
Normal file
185
taskncoffee-app/src/components/ui/shadcn-io/menu-dock/index.jsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
'use client';;
|
||||||
|
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
|
import { Home, Briefcase, Calendar, Shield, Settings } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const defaultItems = [
|
||||||
|
{ label: 'home', icon: Home },
|
||||||
|
{ label: 'work', icon: Briefcase },
|
||||||
|
{ label: 'calendar', icon: Calendar },
|
||||||
|
{ label: 'security', icon: Shield },
|
||||||
|
{ label: 'settings', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MenuDock = ({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
orientation = 'horizontal',
|
||||||
|
showLabels = true,
|
||||||
|
animated = true
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const finalItems = useMemo(() => {
|
||||||
|
const isValid = items && Array.isArray(items) && items.length >= 2 && items.length <= 8;
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn(
|
||||||
|
"MenuDock: 'items' prop is invalid or missing. Using default items.",
|
||||||
|
items
|
||||||
|
);
|
||||||
|
return defaultItems;
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
const [underlineWidth, setUnderlineWidth] = useState(0);
|
||||||
|
const [underlineLeft, setUnderlineLeft] = useState(0);
|
||||||
|
|
||||||
|
const textRefs = useRef([]);
|
||||||
|
const itemRefs = useRef([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndex >= finalItems.length) {
|
||||||
|
setActiveIndex(0);
|
||||||
|
}
|
||||||
|
}, [finalItems, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateUnderline = () => {
|
||||||
|
const activeButton = itemRefs.current[activeIndex];
|
||||||
|
const activeText = textRefs.current[activeIndex];
|
||||||
|
|
||||||
|
if (activeButton && activeText && showLabels && orientation === 'horizontal') {
|
||||||
|
const buttonRect = activeButton.getBoundingClientRect();
|
||||||
|
const textRect = activeText.getBoundingClientRect();
|
||||||
|
const containerRect = activeButton.parentElement?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (containerRect) {
|
||||||
|
setUnderlineWidth(textRect.width);
|
||||||
|
setUnderlineLeft(
|
||||||
|
buttonRect.left - containerRect.left + (buttonRect.width - textRect.width) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateUnderline();
|
||||||
|
window.addEventListener('resize', updateUnderline);
|
||||||
|
return () => window.removeEventListener('resize', updateUnderline);
|
||||||
|
}, [activeIndex, finalItems, showLabels, orientation]);
|
||||||
|
|
||||||
|
const handleItemClick = (index, item) => {
|
||||||
|
setActiveIndex(index);
|
||||||
|
item.onClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVariantStyles = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'compact':
|
||||||
|
return {
|
||||||
|
container: 'p-1',
|
||||||
|
item: 'p-2 min-w-12',
|
||||||
|
icon: 'h-4 w-4',
|
||||||
|
text: 'text-xs'
|
||||||
|
};
|
||||||
|
case 'large':
|
||||||
|
return {
|
||||||
|
container: 'p-3',
|
||||||
|
item: 'p-3 min-w-16',
|
||||||
|
icon: 'h-6 w-6',
|
||||||
|
text: 'text-base'
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
container: 'p-2',
|
||||||
|
item: 'p-2 min-w-14',
|
||||||
|
icon: 'h-5 w-5',
|
||||||
|
text: 'text-sm'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = getVariantStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex items-center rounded-xl bg-card border shadow-sm',
|
||||||
|
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
|
||||||
|
styles.container,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="navigation">
|
||||||
|
{finalItems.map((item, index) => {
|
||||||
|
const isActive = index === activeIndex;
|
||||||
|
const IconComponent = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
ref={(el) => { itemRefs.current[index] = el; }}
|
||||||
|
className={cn(
|
||||||
|
'relative flex flex-col items-center justify-center rounded-lg transition-all duration-200',
|
||||||
|
'hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||||
|
styles.item,
|
||||||
|
isActive && 'text-primary',
|
||||||
|
!isActive && 'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
onClick={() => handleItemClick(index, item)}
|
||||||
|
aria-label={item.label}
|
||||||
|
type="button">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center transition-all duration-200',
|
||||||
|
animated && isActive && 'animate-bounce',
|
||||||
|
orientation === 'horizontal' && showLabels ? 'mb-1' : '',
|
||||||
|
orientation === 'vertical' && showLabels ? 'mb-1' : ''
|
||||||
|
)}>
|
||||||
|
<IconComponent className={cn(styles.icon, 'transition-colors duration-200')} />
|
||||||
|
</div>
|
||||||
|
{showLabels && (
|
||||||
|
<span
|
||||||
|
ref={(el) => { textRefs.current[index] = el; }}
|
||||||
|
className={cn(
|
||||||
|
'font-medium transition-colors duration-200 capitalize',
|
||||||
|
styles.text,
|
||||||
|
'whitespace-nowrap'
|
||||||
|
)}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Animated underline for horizontal orientation with labels */}
|
||||||
|
{showLabels && orientation === 'horizontal' && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute bottom-2 h-0.5 bg-primary rounded-full transition-all duration-300 ease-out',
|
||||||
|
animated ? 'transition-all duration-300' : ''
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: `${underlineWidth}px`,
|
||||||
|
left: `${underlineLeft}px`,
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* Active indicator for vertical orientation or no labels */}
|
||||||
|
{(!showLabels || orientation === 'vertical') && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute bg-primary rounded-full transition-all duration-300',
|
||||||
|
orientation === 'vertical'
|
||||||
|
? 'left-1 w-1 h-6'
|
||||||
|
: 'bottom-0.5 h-0.5 w-6'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
[orientation === 'vertical' ? 'top' : 'left']:
|
||||||
|
orientation === 'vertical'
|
||||||
|
? `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
|
||||||
|
: `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
132
taskncoffee-app/src/index.css
Normal file
132
taskncoffee-app/src/index.css
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--primary: oklch(0.21 0.034 264.665);
|
||||||
|
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--secondary: oklch(0.967 0.003 264.542);
|
||||||
|
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--muted: oklch(0.967 0.003 264.542);
|
||||||
|
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||||
|
--accent: oklch(0.967 0.003 264.542);
|
||||||
|
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.928 0.006 264.531);
|
||||||
|
--input: oklch(0.928 0.006 264.531);
|
||||||
|
--ring: oklch(0.707 0.022 261.325);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||||
|
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.13 0.028 261.692);
|
||||||
|
--foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--card: oklch(0.21 0.034 264.665);
|
||||||
|
--card-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--popover: oklch(0.21 0.034 264.665);
|
||||||
|
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--primary: oklch(0.928 0.006 264.531);
|
||||||
|
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--secondary: oklch(0.278 0.033 256.848);
|
||||||
|
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--muted: oklch(0.278 0.033 256.848);
|
||||||
|
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||||
|
--accent: oklch(0.278 0.033 256.848);
|
||||||
|
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.551 0.027 264.364);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
13
taskncoffee-app/src/layouts/AuthLayout.jsx
Normal file
13
taskncoffee-app/src/layouts/AuthLayout.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Outlet } from 'react-router'
|
||||||
|
|
||||||
|
const AuthLayout = ({ className }) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthLayout }
|
||||||
26
taskncoffee-app/src/layouts/DashLayout.jsx
Normal file
26
taskncoffee-app/src/layouts/DashLayout.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
import { MenuDock } from '@/components/ui/shadcn-io/menu-dock';
|
||||||
|
import { Home, Settings, Bell } from 'lucide-react';
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
const sidebarItems = [
|
||||||
|
{ label: 'home', icon: Home, onClick: () => console.log('Home clicked') },
|
||||||
|
{ label: 'notifications', icon: Bell, onClick: () => console.log('Notifications clicked') },
|
||||||
|
{ label: 'settings', icon: Settings, onClick: () => console.log('Settings clicked') },
|
||||||
|
];
|
||||||
|
export default function MenuDockVertical() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[180px] p-4 flex justify-start items-start">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<Avatar className="mb-2 size-10 justify-center items-center">
|
||||||
|
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||||
|
<AvatarFallback>CN</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<MenuDock
|
||||||
|
items={sidebarItems}
|
||||||
|
variant="compact"
|
||||||
|
orientation="vertical"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
taskncoffee-app/src/lib/utils.js
Normal file
6
taskncoffee-app/src/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
13
taskncoffee-app/src/main.jsx
Normal file
13
taskncoffee-app/src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import { BrowserRouter } from 'react-router'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
121
taskncoffee-app/src/neon.css
Normal file
121
taskncoffee-app/src/neon.css
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Vibur';
|
||||||
|
src: url('./assets/fonts/vibur.woff2') format('woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fabulous';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./assets/fonts/Fabulous.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Carry-You';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./assets/fonts/Carry-You.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
/*-- Sign Styles --*/
|
||||||
|
|
||||||
|
.sign {
|
||||||
|
font-family: "Carry-You", cursive;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 5.6rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
color: #c6e2ff;
|
||||||
|
animation: neon .08s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-pink {
|
||||||
|
font-family: "Carry-You", cursive;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 5.6rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
color: #ffc5ec;
|
||||||
|
animation: neon-pink .08s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes neon {
|
||||||
|
from {
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(202,228,225,0.92),
|
||||||
|
0 0 30px rgba(202,228,225,0.34),
|
||||||
|
0 0 12px rgba(30,132,242,0.52),
|
||||||
|
0 0 21px rgba(30,132,242,0.92),
|
||||||
|
0 0 34px rgba(30,132,242,0.78),
|
||||||
|
0 0 54px rgba(30,132,242,0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(202,228,225,0.98),
|
||||||
|
0 0 30px rgba(202,228,225,0.42),
|
||||||
|
0 0 12px rgba(30,132,242,0.58),
|
||||||
|
0 0 22px rgba(30,132,242,0.84),
|
||||||
|
0 0 38px rgba(30,132,242,0.88),
|
||||||
|
0 0 60px rgba(30,132,242,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-pink {
|
||||||
|
from {
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(255,182,193,0.92),
|
||||||
|
0 0 30px rgba(255,182,193,0.34),
|
||||||
|
0 0 12px rgba(255,20,147,0.52),
|
||||||
|
0 0 21px rgba(255,20,147,0.92),
|
||||||
|
0 0 34px rgba(255,20,147,0.78),
|
||||||
|
0 0 54px rgba(255,20,147,0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(255,182,193,0.98),
|
||||||
|
0 0 30px rgba(255,182,193,0.42),
|
||||||
|
0 0 12px rgba(255,20,147,0.58),
|
||||||
|
0 0 22px rgba(255,20,147,0.84),
|
||||||
|
0 0 38px rgba(255,20,147,0.88),
|
||||||
|
0 0 60px rgba(255,20,147,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px; left: 10px;
|
||||||
|
color: #828282;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: #c6e2ff;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(202,228,225,0.92),
|
||||||
|
0 0 10px rgba(202,228,225,0.34),
|
||||||
|
0 0 4px rgba(30,132,242,0.52),
|
||||||
|
0 0 7px rgba(30,132,242,0.92),
|
||||||
|
0 0 11px rgba(30,132,242,0.78),
|
||||||
|
0 0 16px rgba(30,132,242,0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
taskncoffee-app/src/pages/Login.jsx
Normal file
119
taskncoffee-app/src/pages/Login.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { login } from "@/apiv1/auth.service"
|
||||||
|
import { Link } from "react-router"
|
||||||
|
|
||||||
|
|
||||||
|
export function LoginPage({ className }) {
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [success, setSuccess] = useState("")
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await login(username, password)
|
||||||
|
setSuccess("Login successful!")
|
||||||
|
console.log("Logged in:", result)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.detail || "Login failed. Please check your credentials.")
|
||||||
|
console.error("Login error:", err)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Card className="w-full max-w-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Login to your account</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your username below to login to your account
|
||||||
|
</CardDescription>
|
||||||
|
<CardAction>
|
||||||
|
<Button variant="link">
|
||||||
|
<Link to="/auth/signup">Sign Up</Link>
|
||||||
|
</Button>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{error && (
|
||||||
|
<div className="text-sm text-red-500 bg-red-50 p-2 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="text-sm text-green-500 bg-green-50 p-2 rounded">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Forgot your password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Loading..." : "Login"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
67
taskncoffee-app/src/pages/SignUp.jsx
Normal file
67
taskncoffee-app/src/pages/SignUp.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Link } from "react-router"
|
||||||
|
|
||||||
|
const SignUp = ({
|
||||||
|
heading = "Signup",
|
||||||
|
logo = {
|
||||||
|
// url: "https://www.shadcnblocks.com",
|
||||||
|
// src: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/logos/shadcnblockscom-wordmark.svg",
|
||||||
|
alt: "logo",
|
||||||
|
title: "shadcnblocks.com",
|
||||||
|
},
|
||||||
|
buttonText = "Create Account",
|
||||||
|
signupText = "Already a user?",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section className="bg-muted h-screen">
|
||||||
|
<div className="flex h-full items-center justify-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex flex-col items-center gap-6 lg:justify-start">
|
||||||
|
<a href={logo.url}>
|
||||||
|
<img
|
||||||
|
src={logo.src}
|
||||||
|
alt={logo.alt}
|
||||||
|
title={logo.title}
|
||||||
|
className="h-10 dark:invert"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div className="min-w-sm border-muted bg-background flex w-full max-w-sm flex-col items-center gap-y-4 rounded-md border px-6 py-8 shadow-md">
|
||||||
|
{heading && <h1 className="text-xl font-semibold">{heading}</h1>}
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
className="text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
className="text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
className="text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex justify-center gap-1 text-sm">
|
||||||
|
<p>{signupText}</p>
|
||||||
|
<a
|
||||||
|
className="text-primary font-medium hover:underline"
|
||||||
|
>
|
||||||
|
<Link to="/auth/login">Login</Link>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SignUp };
|
||||||
13
taskncoffee-app/vite.config.js
Normal file
13
taskncoffee-app/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import path from "path"
|
||||||
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user