Compare commits

..

22 Commits

Author SHA1 Message Date
IluaAir
6712c189c0 Enhance dashboard layout and styles with task grouping and priority indicators 2025-10-15 20:04:34 +03:00
IluaAir
b816906c5c tasks 2025-10-15 10:07:28 +03:00
IluaAir
c647f837e8 dashboard css 2025-10-13 23:24:51 +03:00
IluaAir
80b40ce1c0 off animation 2025-10-12 23:38:22 +03:00
IluaAir
36926c9974 inline for neon and update dashboard styles 2025-10-12 23:29:16 +03:00
IluaAir
be3181ca00 menydesk material-desgn 3 2025-10-12 23:20:03 +03:00
IluaAir
8ee524318c menydock update, dashboard page create 2025-10-12 22:52:48 +03:00
IluaAir
2c35b781dd start creating dashboard 2025-10-10 23:58:57 +03:00
IluaAir
31e8e355f3 add jwt-decode 2025-10-10 00:07:38 +03:00
IluaAir
23945b3487 fix layout 2025-10-09 23:55:20 +03:00
IluaAir
b4f98fe6cd navigation login and signup 2025-10-09 23:51:00 +03:00
IluaAir
1f351bc32b router and fix backend cors 2025-10-09 23:31:32 +03:00
IluaAir
3708a612f7 fix api sources 2025-10-09 23:00:57 +03:00
IluaAir
4fe2d39fba login axios 2025-10-09 22:51:50 +03:00
IluaAir
15426adc69 add auth service 2025-10-06 23:32:34 +03:00
IluaAir
eee26d06b7 add fingerprint 2025-10-06 23:09:38 +03:00
IluaAir
e0f22b6c5a add axios client with interceptors 2025-10-06 23:09:25 +03:00
IluaAir
4a33b8defa add api sources 2025-10-06 00:41:57 +03:00
IluaAir
7978b1042d ripple button 2025-10-05 00:16:20 +03:00
IluaAir
33bd88629f add login card and shadcn components 2025-10-04 23:54:19 +03:00
IluaAir
e41b8922e5 add neon and font style 2025-10-04 02:04:51 +03:00
IluaAir
4211be86ff init frontend 2025-10-04 00:03:42 +03:00
38 changed files with 7607 additions and 0 deletions

View File

@@ -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="__"
) )

View File

@@ -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
View 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
View 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.

View 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": {}
}

View 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 },
],
},
},
])

View 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>

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

5776
taskncoffee-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

View File

@@ -0,0 +1,30 @@
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 Dashboard from './pages/Dashboard'
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={
<Dashboard />
} />
<Route path="*" element={<Navigate to="/auth/login" replace />} />
</Routes>
)
}
export default App

View 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 };

View 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;
}
};

View 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;

View 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');
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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 }
}

View 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>
)
}

View 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 }

View 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 }

View 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,
}

View 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 }

View 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 }

View File

@@ -0,0 +1,189 @@
'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,
showIndicator = 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 gap-3',
item: 'px-5 py-2',
icon: 'h-4 w-4',
text: 'text-xs'
};
case 'large':
return {
container: 'p-3 gap-4',
item: 'px-6 py-4',
icon: 'h-6 w-6',
text: 'text-base'
};
default:
return {
container: 'p-2 gap-3',
item: 'px-5 py-3.5',
icon: 'h-5 w-5',
text: 'text-sm'
};
}
};
const styles = getVariantStyles();
return (
<nav
className={cn(
'relative inline-flex items-center rounded-3xl bg-transparent',
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
styles.container,
className
)}
role="navigation">
{finalItems.map((item, index) => {
const isActive = index === activeIndex;
const IconComponent = item.icon;
return (
<div
key={`${item.label}-${index}`}
className="flex flex-col items-center gap-1">
<button
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
'relative flex items-center justify-center rounded-full transition-all duration-300',
'hover:bg-primary/10 focus-visible:outline-none',
'select-none active:scale-95',
styles.item,
isActive && 'text-primary bg-primary/15',
!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'
)}>
<IconComponent className={cn(styles.icon, 'transition-colors duration-200')} />
</div>
</button>
{showLabels && (
<span
ref={(el) => { textRefs.current[index] = el; }}
className={cn(
'font-medium transition-colors duration-200 capitalize',
styles.text,
'whitespace-nowrap',
isActive ? 'text-primary' : 'text-muted-foreground'
)}>
{item.label}
</span>
)}
</div>
);
})}
{/* Animated underline for horizontal orientation with labels */}
{showIndicator && 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 */}
{showIndicator && (!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>
);
};

View File

@@ -0,0 +1,133 @@
@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;
}
}

View 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 }

View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

View 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>,
)

View File

@@ -0,0 +1,138 @@
@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;
}
}
/*-- Inline Sign Styles for Headers --*/
.sign-inline {
font-family: "Carry-You", cursive;
font-size: 3rem;
line-height: 1;
color: #c6e2ff;
animation: neon .08s ease-in-out infinite alternate;
}
.sign-pink-inline {
font-family: "Carry-You", cursive;
font-size: 3rem;
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);
}
}

View File

@@ -0,0 +1,149 @@
'use client';
import '../neon.css';
import './dashboard.css';
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 menuItems = [
{ label: 'home', icon: Home, onClick: () => console.log('Home clicked') },
{ label: 'notify', icon: Bell, onClick: () => console.log('Notifications clicked') },
{ label: 'settings', icon: Settings, onClick: () => console.log('Settings clicked') },
];
export default function Dashboard() {
// Mock data from backend
const tasksFromBackend = [
{ title: "testtitle", due_date: "2025-10-14", priority: "medium", id: 1, status: "open" },
{ title: "title", due_date: "2025-10-15", priority: "high", id: 3, status: "open" },
{ title: "title title2", due_date: "2025-10-15", priority: "medium", id: 4, status: "closed" },
{ title: "title title3", due_date: "2025-10-15", priority: "medium", id: 5, status: "open" },
{ title: "title16", due_date: "2025-10-15", priority: "medium", id: 6, status: "completed" },
{ title: "super late title", due_date: "2025-10-15", priority: "low", id: 2, status: "open" }
];
// Get current date and week
const today = new Date();
const currentDay = today.getDay();
// Calculate the Monday of current week
const currentDate = new Date(today);
const dayOffset = currentDay === 0 ? -6 : 1 - currentDay;
currentDate.setDate(today.getDate() + dayOffset);
// Group tasks by day
const groupTasksByDay = (tasks) => {
const grouped = {};
tasks.forEach(task => {
const taskDate = new Date(task.due_date);
const dateKey = taskDate.toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
grouped[dateKey].push({
id: task.id,
title: task.title,
priority: task.priority,
completed: task.status === 'closed' || task.status === 'completed',
dueDate: task.due_date
});
});
return grouped;
};
const groupedTasks = groupTasksByDay(tasksFromBackend);
// Generate days of week with dates and tasks
const daysOfWeek = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
].map((name, index) => {
const date = new Date(currentDate);
date.setDate(currentDate.getDate() + index);
const dateKey = date.toDateString();
return {
name,
date: date.getDate(),
month: date.toLocaleDateString('en-US', { month: 'short' }),
fullDate: date,
tasks: groupedTasks[dateKey] || []
};
});
return (
<div className="dashboard-container">
{/* Navigation Rail - Material Design 3 */}
<aside className="dashboard-sidebar">
{/* Avatar with large M3 container */}
<div className="dashboard-avatar-container">
<Avatar className="dashboard-avatar">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback className="dashboard-avatar-fallback">CN</AvatarFallback>
</Avatar>
</div>
<MenuDock
items={menuItems}
orientation="vertical"
animated={false}
showIndicator={false}
showLabels={true}
variant="compact"
/>
</aside>
{/* Main Content Area */}
<main className="dashboard-main">
<div className="dashboard-content">
<h1 className="dashboard-header">
<span className="sign-pink-inline">Task&</span>
<span className="sign-inline">Coffee</span>
</h1>
<div className="dashboard-surface-container">
<div className="dashboard-week-grid border-1">
{daysOfWeek.map((day, index) => {
const isToday = day.fullDate.toDateString() === today.toDateString();
return (
<div
key={day.name}
className={`dashboard-day-column ${isToday ? 'today' : ''} dashboard-card-filled`}
>
<div className="dashboard-day-header">
<h3 className="dashboard-day-title">{day.name}</h3>
<p className="dashboard-day-date">{day.month} {day.date}</p>
</div>
<div className="dashboard-tasks-container">
{day.tasks.length > 0 ? (
day.tasks.map((task) => (
<div
key={task.id}
className={`dashboard-task-card ${task.completed ? 'completed' : ''} priority-${task.priority}`}
>
<div className="dashboard-task-header">
<span className={`dashboard-task-priority ${task.priority}`}></span>
<p className="dashboard-task-title">{task.title}</p>
</div>
</div>
))
) : (
<p className="dashboard-no-tasks">No tasks</p>
)}
</div>
</div>
);
})}
</div>
</div>
<div className="dashboard-spacer"></div>
</div>
</main>
</div>
);
}

View 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>
)
}

View 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 };

View File

@@ -0,0 +1,259 @@
/* Dashboard Layout */
.dashboard-container {
display: flex;
min-height: 100vh;
background: var(--color-background);
}
/* Navigation Rail - Material Design 3 */
.dashboard-sidebar {
width: 80px;
background: color-mix(in srgb, var(--color-card) 50%, transparent);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 1rem;
padding-bottom: 1rem;
gap: 1rem;
}
.dashboard-avatar-container {
margin-bottom: 0.5rem;
}
.dashboard-avatar {
width: 3.5rem;
height: 3.5rem;
border-radius: 1rem;
}
.dashboard-avatar-fallback {
border-radius: 1rem;
}
/* Main Content Area */
.dashboard-main {
flex: 1;
padding: 1.5rem;
background: var(--color-background);
display: flex;
flex-direction: column;
}
.dashboard-content {
max-width: 80rem;
margin-left: auto;
margin-right: auto;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
gap: 1.5rem;
}
/* Header */
.dashboard-header {
font-size: 1.875rem;
font-weight: bold;
}
/* Material Design 3 Cards Grid */
.dashboard-cards-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.dashboard-cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.dashboard-cards-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* M3 Card Variants */
.dashboard-card-filled {
background: var(--color-card);
padding: 1.5rem;
border-radius: 1.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.dashboard-card-elevated {
background: var(--color-card);
padding: 1.5rem;
border-radius: 1.5rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
transition: box-shadow 0.3s;
}
.dashboard-card-elevated:hover {
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
.dashboard-card-outlined {
background: var(--color-background);
border: 2px solid var(--color-border);
padding: 1.5rem;
border-radius: 1.5rem;
}
.dashboard-card-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dashboard-card-text {
color: var(--color-muted-foreground);
font-size: 0.875rem;
}
/* Surface Container */
.dashboard-surface-container {
background: color-mix(in srgb, var(--color-card) 30%, transparent);
padding: 1.5rem;
border-radius: 1.5rem;
flex: 3;
}
.dashboard-surface-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.dashboard-surface-text {
color: var(--color-muted-foreground);
}
/* Spacer */
.dashboard-spacer {
flex: 0.5;
}
/* Weekly View */
.dashboard-week-grid {
display: flex;
gap: 1rem;
height: 100%;
overflow-x: auto;
padding: 0.6rem;
}
.dashboard-day-column {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
background: var(--color-background);
border-radius: 1rem;
padding: 1rem;
transition: all 0.3s ease;
}
.dashboard-day-column.today {
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
border: 2px solid var(--color-primary);
}
.dashboard-day-header {
text-align: center;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0.75rem;
}
.dashboard-day-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
}
.dashboard-day-date {
font-size: 0.875rem;
color: var(--color-muted-foreground);
margin: 0;
}
.dashboard-day-column.today .dashboard-day-title {
color: var(--color-primary);
}
.dashboard-day-column.today .dashboard-day-date {
color: var(--color-primary);
font-weight: 600;
}
.dashboard-tasks-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
}
.dashboard-task-card {
background: var(--color-card);
padding: 0.75rem;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
font-size: 0.875rem;
transition: transform 0.2s;
}
.dashboard-task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.dashboard-task-card.completed {
opacity: 0.6;
text-decoration: line-through;
}
.dashboard-task-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dashboard-task-priority {
width: 4px;
height: 4px;
border-radius: 50%;
flex-shrink: 0;
}
.dashboard-task-priority.low {
background: #10b981; /* green */
}
.dashboard-task-priority.medium {
background: #f59e0b; /* orange */
}
.dashboard-task-priority.high {
background: #ef4444; /* red */
}
.dashboard-task-title {
margin: 0;
font-size: 0.875rem;
color: var(--color-foreground);
flex: 1;
}
.dashboard-no-tasks {
color: var(--color-muted-foreground);
font-size: 0.875rem;
text-align: center;
padding: 2rem 0;
font-style: italic;
}

View 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"),
},
},
})