feat(week8): JWT auth, SSM Parameter Store, and UI overhaul

This commit is contained in:
ikechukwu Elizabeth
2026-05-30 07:06:21 +01:00
parent 0de2d1dd42
commit dcaef8ff13
49 changed files with 8868 additions and 723 deletions
+57 -148
View File
@@ -1,155 +1,64 @@
import { useEffect, useState } from 'react'
const API = '/api'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import Landing from './pages/Landing'
import Login from './pages/Login'
import Register from './pages/Register'
import Dashboard from './pages/Dashboard'
import Projects from './pages/Projects'
import Tasks from './pages/Tasks'
import Teams from './pages/Teams'
import Calendar from './pages/Calendar'
import Activity from './pages/Activity'
import Notifications from './pages/Notifications'
import Analytics from './pages/Analytics'
import Settings from './pages/Settings'
import Pricing from './pages/Pricing'
import About from './pages/About'
import Contact from './pages/Contact'
import Status from './pages/Status'
import TaskManagement from './pages/features/TaskManagement'
import TeamCollaboration from './pages/features/TeamCollaboration'
import NotificationsFeature from './pages/features/Notifications'
import ProjectWorkspaces from './pages/features/ProjectWorkspaces'
import AnalyticsFeature from './pages/features/Analytics'
import Security from './pages/features/Security'
import Architecture from './pages/features/Architecture'
export default function App() {
const [tasks, setTasks] = useState([])
const [title, setTitle] = useState('')
const [loading, setLoading] = useState(true)
const [adding, setAdding] = useState(false)
const fetchTasks = async () => {
const res = await fetch(`${API}/tasks`)
const data = await res.json()
setTasks(data)
setLoading(false)
}
useEffect(() => { fetchTasks() }, [])
const addTask = async () => {
if (!title.trim()) return
setAdding(true)
await fetch(`${API}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
})
setTitle('')
setAdding(false)
fetchTasks()
}
const toggleTask = async (id) => {
await fetch(`${API}/tasks/${id}`, { method: 'PATCH' })
fetchTasks()
}
const deleteTask = async (id) => {
await fetch(`${API}/tasks/${id}`, { method: 'DELETE' })
fetchTasks()
}
const completed = tasks.filter(t => t.completed).length
const remaining = tasks.filter(t => !t.completed).length
const progress = tasks.length > 0 ? (completed / tasks.length) * 100 : 0
return (
<div className="app">
<BrowserRouter>
<Routes>
{/* Public */}
<Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/status" element={<Status />} />
{/* LEFT PANEL */}
<div className="sidebar">
<div className="sidebar-top">
<div className="brand">
<div className="brand-icon">✓</div>
<div>
<h1>TaskFlow</h1>
<p className="brand-sub">Personal workspace</p>
</div>
</div>
{/* Feature detail pages */}
<Route path="/features/task-management" element={<TaskManagement />} />
<Route path="/features/team-collaboration" element={<TeamCollaboration />} />
<Route path="/features/notifications" element={<NotificationsFeature />} />
<Route path="/features/project-workspaces" element={<ProjectWorkspaces />} />
<Route path="/features/analytics" element={<AnalyticsFeature />} />
<Route path="/features/security" element={<Security />} />
<Route path="/features/architecture" element={<Architecture />} />
<div className="stats">
<div className="stat-card">
<span className="stat-number">{tasks.length}</span>
<span className="stat-label">Total</span>
</div>
<div className="stat-card accent">
<span className="stat-number">{remaining}</span>
<span className="stat-label">Remaining</span>
</div>
<div className="stat-card green">
<span className="stat-number">{completed}</span>
<span className="stat-label">Done</span>
</div>
</div>
{/* Dashboard pages */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/projects" element={<Projects />} />
<Route path="/dashboard/tasks" element={<Tasks />} />
<Route path="/dashboard/teams" element={<Teams />} />
<Route path="/dashboard/calendar" element={<Calendar />} />
<Route path="/dashboard/activity" element={<Activity />} />
<Route path="/dashboard/notifications" element={<Notifications />} />
<Route path="/dashboard/analytics" element={<Analytics />} />
<Route path="/dashboard/settings" element={<Settings />} />
<div className="progress-section">
<div className="progress-header">
<span>Progress</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
</div>
<div className="sidebar-footer">
<p className="built-by">Built with Node.js · Docker · Nginx</p>
<p className="built-by">Deployed via Docker Compose</p>
</div>
</div>
{/* RIGHT PANEL */}
<div className="main">
<div className="main-header">
<div>
<h2>My Tasks</h2>
<p className="main-sub">Stay focused. Ship things.</p>
</div>
</div>
<div className="input-area">
<input
value={title}
onChange={e => setTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTask()}
placeholder="What needs to be done? Press Enter or click Add..."
/>
<button className="btn-add" onClick={addTask} disabled={adding}>
{adding ? '...' : '+ Add Task'}
</button>
</div>
<div className="task-section">
{loading && <p className="empty">Loading your tasks...</p>}
{!loading && tasks.length === 0 && (
<div className="empty-state">
<div className="empty-icon">📋</div>
<p className="empty-title">No tasks yet</p>
<p className="empty-desc">Add your first task above to get started</p>
</div>
)}
{tasks.filter(t => !t.completed).length > 0 && (
<div className="task-group">
<p className="group-label">● Active</p>
{tasks.filter(t => !t.completed).map(task => (
<div key={task.id} className="task-item">
<button className="check-btn" onClick={() => toggleTask(task.id)} title="Mark complete" />
<span className="task-title">{task.title}</span>
<button className="delete-btn" onClick={() => deleteTask(task.id)}>Delete</button>
</div>
))}
</div>
)}
{tasks.filter(t => t.completed).length > 0 && (
<div className="task-group">
<p className="group-label done-label">✓ Completed</p>
{tasks.filter(t => t.completed).map(task => (
<div key={task.id} className="task-item completed">
<button className="check-btn checked" onClick={() => toggleTask(task.id)} title="Mark incomplete">✓</button>
<span className="task-title">{task.title}</span>
<button className="delete-btn" onClick={() => deleteTask(task.id)}>Delete</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
)
}
}
@@ -0,0 +1,93 @@
/* Reusable empty state components
* Usage: <EmptyTasks onAdd={fn} /> <EmptyProjects onAdd={fn} /> etc.
*/
function EmptyBase({ icon, title, description, ctaLabel, onAction }) {
return (
<div className="flex flex-col items-center justify-center py-20 px-6 text-center animate-fade-up">
<div className="w-16 h-16 rounded-2xl bg-surface2 border border-border flex items-center justify-center mb-5 text-2xl">
{icon}
</div>
<h3 className="font-display font-700 text-text1 text-lg mb-2">{title}</h3>
<p className="text-sm text-text2 max-w-xs leading-relaxed mb-6">{description}</p>
{ctaLabel && (
<button
onClick={onAction}
className="px-5 py-2.5 bg-accent hover:bg-accent-hover text-white text-sm font-600 rounded-lg transition-all duration-150 shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5 active:translate-y-0"
>
{ctaLabel}
</button>
)}
</div>
)
}
export function EmptyTasks({ onAdd }) {
return (
<EmptyBase
icon={
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
}
title="No Tasks Yet"
description="You don't have any tasks. Add your first task to start tracking your work and making progress."
ctaLabel="+ Add Your First Task"
onAction={onAdd}
/>
)
}
export function EmptyProjects({ onAdd }) {
return (
<EmptyBase
icon={
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 7a2 2 0 012-2h4l2 3h9a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V7z"/>
</svg>
}
title="No Projects Found"
description="You haven't created any projects yet. Organize your tasks into projects to track progress across different workstreams."
ctaLabel="Create First Project"
onAction={onAdd}
/>
)
}
export function EmptyNotifications({ }) {
return (
<EmptyBase
icon={
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 01-3.46 0"/>
</svg>
}
title="No Notifications"
description="You're all caught up. New activity from your projects, team, and tasks will appear here."
/>
)
}
export function EmptyTeam({ onInvite }) {
return (
<EmptyBase
icon={
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 00-3-3.87"/>
<path d="M16 3.13a4 4 0 010 7.75"/>
</svg>
}
title="No Team Members Added"
description="Your workspace is currently solo. Invite your team to collaborate on tasks, share projects, and move faster together."
ctaLabel="Invite Team Member"
onAction={onInvite}
/>
)
}
/* Default export for convenience */
export default { EmptyTasks, EmptyProjects, EmptyNotifications, EmptyTeam }
@@ -0,0 +1,37 @@
import { Link } from 'react-router-dom'
export default function FeatureCard({ title, description, icon, color, bg, to, index = 0 }) {
return (
<Link
to={to}
className="group block p-5 rounded-xl border bg-surface hover:bg-surface2 transition-all duration-200 hover:-translate-y-1.5 hover:shadow-xl hover:shadow-accent/10 cursor-pointer animate-fade-up"
style={{ animationDelay: `${index * 0.07}s` }}
>
{/* Icon */}
<div className={`w-11 h-11 rounded-xl border flex items-center justify-center mb-4 ${bg} ${color} transition-all duration-200 group-hover:scale-110 group-hover:shadow-lg`}>
{icon}
</div>
{/* Title */}
<h3 className="font-display font-700 text-text1 text-sm mb-2 tracking-tight group-hover:text-accent transition-colors duration-150">
{title}
</h3>
{/* Description */}
<p className="text-xs text-text2 leading-relaxed mb-4">{description}</p>
{/* Learn more link */}
<div className="flex items-center gap-1.5 text-xs font-600 text-text3 group-hover:text-accent transition-colors duration-150">
<span>Learn more</span>
<svg
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
className="transition-transform duration-150 group-hover:translate-x-1"
>
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12 5 19 12 12 19"/>
</svg>
</div>
</Link>
)
}
@@ -0,0 +1,78 @@
import { Link } from 'react-router-dom'
import Footer from './Footer'
export default function FeaturePageLayout({ hero, children }) {
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</Link>
<nav className="hidden md:flex items-center gap-6">
<Link to="/#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</Link>
<Link to="/dashboard" className="text-sm text-text2 hover:text-text1 transition-colors">Dashboard</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Breadcrumb */}
<div className="border-b border-border">
<div className="max-w-6xl mx-auto px-6 py-3 flex items-center gap-2 text-xs text-text3">
<Link to="/" className="hover:text-text2 transition-colors">Home</Link>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="9 18 15 12 9 6"/></svg>
<Link to="/#features" className="hover:text-text2 transition-colors">Features</Link>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="9 18 15 12 9 6"/></svg>
<span className="text-text2">{hero.title}</span>
</div>
</div>
{/* Hero */}
<section className="relative overflow-hidden border-b border-border">
<div className="absolute inset-0 bg-[linear-gradient(rgba(99,102,241,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(99,102,241,0.04)_1px,transparent_1px)] bg-[size:60px_60px]" />
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-accent/8 rounded-full blur-[100px] translate-x-1/2 -translate-y-1/2 pointer-events-none" />
<div className="relative z-10 max-w-6xl mx-auto px-6 py-20">
<div className="flex items-center gap-3 mb-6">
<div className={`w-12 h-12 rounded-2xl border flex items-center justify-center ${hero.bg} ${hero.color}`}>
{hero.icon}
</div>
<span className="text-xs font-700 text-accent uppercase tracking-widest">{hero.category}</span>
</div>
<h1 className="font-display font-800 text-4xl md:text-5xl text-text1 leading-tight tracking-tight mb-5 max-w-3xl">
{hero.title}
</h1>
<p className="text-base text-text2 leading-relaxed max-w-2xl mb-8">{hero.description}</p>
<div className="flex gap-4">
<Link to="/register" className="px-6 py-3 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Free
</Link>
<Link to="/dashboard" className="px-6 py-3 bg-surface hover:bg-surface2 text-text1 font-600 rounded-xl border border-border text-sm transition-all">
View Demo
</Link>
</div>
</div>
</section>
{/* Page content */}
<main className="flex-1">
{children}
</main>
<Footer />
</div>
)
}
+136
View File
@@ -0,0 +1,136 @@
import { Link } from 'react-router-dom'
const FOOTER_LINKS = {
Product: [
{ label: 'Features', to: '/#features' },
{ label: 'Dashboard', to: '/dashboard' },
{ label: 'Pricing', to: '/pricing' },
],
Company: [
{ label: 'About', to: '/about' },
{ label: 'Contact', to: '/contact' },
],
Resources: [
{ label: 'Status', to: '/status' },
{ label: 'GitHub', href: 'https://github.com/elizabeth-ikechukwu/scalable-task-platform' },
],
}
function LinkedInIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2z"/>
<circle cx="4" cy="4" r="2"/>
</svg>
)
}
function GitHubIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
</svg>
)
}
export default function Footer() {
return (
<footer className="bg-surface border-t border-border mt-auto">
<div className="max-w-6xl mx-auto px-8 py-16">
{/* Top row */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-12 mb-12">
{/* Brand block */}
<div className="md:col-span-2">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-accent flex items-center justify-center glow-pulse flex-shrink-0">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 text-lg tracking-tight">TaskFlow</span>
</div>
<p className="text-sm text-text2 leading-relaxed max-w-xs">
A scalable task management platform designed to simplify collaboration, improve productivity, and help teams deliver work efficiently at any scale.
</p>
{/* Social icons - LinkedIn and GitHub only */}
<div className="flex items-center gap-3 mt-6">
<a
href="https://www.linkedin.com/in/ikechukwu-elizabeth"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
className="w-9 h-9 rounded-lg bg-surface2 border border-border text-text3 hover:text-accent hover:border-accent/30 hover:bg-accent-dim flex items-center justify-center transition-all duration-150"
>
<LinkedInIcon />
</a>
<a
href="https://github.com/elizabeth-ikechukwu"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="w-9 h-9 rounded-lg bg-surface2 border border-border text-text3 hover:text-accent hover:border-accent/30 hover:bg-accent-dim flex items-center justify-center transition-all duration-150"
>
<GitHubIcon />
</a>
</div>
</div>
{/* Nav link groups */}
{Object.entries(FOOTER_LINKS).map(([group, links]) => (
<div key={group}>
<p className="text-[0.7rem] font-700 text-text3 uppercase tracking-widest mb-4">{group}</p>
<ul className="flex flex-col gap-2.5">
{links.map(({ label, to, href }) => (
<li key={label}>
{href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-text2 hover:text-text1 transition-colors duration-150"
>
{label}
</a>
) : (
<Link
to={to}
className="text-sm text-text2 hover:text-text1 transition-colors duration-150"
>
{label}
</Link>
)}
</li>
))}
</ul>
</div>
))}
</div>
{/* Bottom row */}
<div className="pt-8 border-t border-border flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-xs text-text3">
&copy; {new Date().getFullYear()} TaskFlow. Built by{' '}
<a
href="https://www.linkedin.com/in/ikechukwu-elizabeth"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:text-accent-hover transition-colors"
>
Elizabeth Ikechukwu
</a>
.
</p>
<div className="flex items-center gap-1.5 text-xs text-text3">
<span className="w-1.5 h-1.5 rounded-full bg-green animate-pulse inline-block" />
All systems operational
</div>
</div>
</div>
</footer>
)
}
+205
View File
@@ -0,0 +1,205 @@
import { NavLink, useNavigate } from 'react-router-dom'
const NAV_ITEMS = [
{
label: 'Dashboard',
to: '/dashboard',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
),
},
{
label: 'Projects',
to: '/dashboard/projects',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 7a2 2 0 012-2h4l2 3h9a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V7z"/>
</svg>
),
},
{
label: 'Tasks',
to: '/dashboard/tasks',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
),
},
{
label: 'Teams',
to: '/dashboard/teams',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 00-3-3.87"/>
<path d="M16 3.13a4 4 0 010 7.75"/>
</svg>
),
},
{
label: 'Calendar',
to: '/dashboard/calendar',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
),
},
{
label: 'Activity',
to: '/dashboard/activity',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
),
},
{
label: 'Notifications',
to: '/dashboard/notifications',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 01-3.46 0"/>
</svg>
),
badge: 3,
},
{
label: 'Analytics',
to: '/dashboard/analytics',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
),
},
{
label: 'Settings',
to: '/dashboard/settings',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>
</svg>
),
},
]
function getUserFromToken() {
try {
const token = localStorage.getItem('token')
if (!token) return null
const payload = JSON.parse(atob(token.split('.')[1]))
return payload
} catch {
return null
}
}
export default function Sidebar() {
const navigate = useNavigate()
const user = getUserFromToken()
const handleLogout = () => {
localStorage.removeItem('token')
navigate('/')
}
const initials = user?.name
? user.name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2)
: 'U'
return (
<aside className="fixed left-0 top-0 h-screen w-[240px] bg-surface border-r border-border flex flex-col z-40">
<div className="px-6 py-6 border-b border-border">
<NavLink to="/dashboard" className="flex items-center gap-3 group">
<div className="w-9 h-9 rounded-xl bg-accent flex items-center justify-center flex-shrink-0 glow-pulse">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<div>
<p className="font-display font-800 text-text1 text-[1rem] leading-none tracking-tight">TaskFlow</p>
<p className="text-[0.65rem] text-text3 font-500 mt-1 uppercase tracking-widest">Workspace</p>
</div>
</NavLink>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-4">
<p className="px-3 mb-2 text-[0.62rem] font-700 text-text3 uppercase tracking-widest">Menu</p>
<ul className="flex flex-col gap-0.5">
{NAV_ITEMS.map((item, i) => (
<li key={item.label}>
<NavLink
to={item.to}
end={item.to === '/dashboard'}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-500 transition-all duration-150 group relative
${isActive
? 'bg-accent-dim text-accent border border-accent/20'
: 'text-text2 hover:text-text1 hover:bg-surface2 border border-transparent'
}`
}
style={{ animationDelay: `${i * 0.04}s` }}
>
<span className="flex-shrink-0">{item.icon}</span>
<span className="flex-1">{item.label}</span>
{item.badge && (
<span className="bg-accent text-white text-[0.6rem] font-700 px-1.5 py-0.5 rounded-full leading-none">
{item.badge}
</span>
)}
</NavLink>
</li>
))}
</ul>
</nav>
<div className="px-4 py-4 border-t border-border">
<div className="flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-surface2 transition-colors group mb-2">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-accent to-green flex items-center justify-center text-white text-xs font-700 flex-shrink-0">
{initials}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-600 text-text1 truncate">{user?.name || 'Guest'}</p>
<p className="text-[0.65rem] text-text3 truncate">{user?.email || 'Not signed in'}</p>
</div>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 px-2 py-2 rounded-lg text-xs font-600 text-text3 hover:text-red hover:bg-red-dim border border-transparent hover:border-red/20 transition-all duration-150"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
Sign out
</button>
<p className="mt-3 px-2 text-[0.62rem] text-text3 leading-relaxed">
Node.js - Docker - Nginx<br/>
Deployed on AWS EC2
</p>
</div>
</aside>
)
}
+91 -348
View File
@@ -1,383 +1,126 @@
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:wght@300;400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ---- CSS variables ---- */
:root {
--bg: #0a0b0f;
--surface: #111318;
--surface2: #16181f;
--border: #1e2028;
--accent: #6366f1;
--accent-dim: rgba(99,102,241,0.12);
--green: #22c55e;
--green-dim: rgba(34,197,94,0.12);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--text: #f1f5f9;
--text-2: #94a3b8;
--text-3: #3d4257;
--bg: #0a0b0f;
--surface: #111318;
--surface2: #16181f;
--border: #1e2028;
--accent: #6366f1;
--accent-dim: rgba(99,102,241,0.12);
--green: #22c55e;
--green-dim: rgba(34,197,94,0.12);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--text: #f1f5f9;
--text-2: #94a3b8;
--text-3: #3d4257;
}
/* ---- Base resets ---- */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html { scroll-behavior: smooth; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* LAYOUT */
.app {
display: grid;
grid-template-columns: 300px 1fr;
min-height: 100vh;
/* ---- Scrollbar ---- */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
/* ---- Selection ---- */
::selection {
background: rgba(99,102,241,0.3);
color: #fff;
}
/* SIDEBAR */
.sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 36px 28px;
}
.sidebar-top { display: flex; flex-direction: column; gap: 36px; }
.brand { display: flex; align-items: center; gap: 14px; }
.brand-icon {
width: 44px;
height: 44px;
background: var(--accent);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
font-weight: 800;
color: white;
flex-shrink: 0;
box-shadow: 0 0 24px rgba(99,102,241,0.35);
font-family: 'Syne', sans-serif;
}
h1 {
font-family: 'Syne', sans-serif;
font-size: 1.2rem;
font-weight: 800;
color: var(--text);
letter-spacing: -0.5px;
}
.brand-sub {
font-size: 0.72rem;
color: var(--text-3);
font-weight: 500;
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.8px;
}
/* STATS */
.stats {
display: flex;
flex-direction: column;
gap: 10px;
}
.stat-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.stat-card:hover { border-color: var(--text-3); }
.stat-card.accent { border-color: rgba(99,102,241,0.3); background: var(--accent-dim); }
.stat-card.green { border-color: rgba(34,197,94,0.3); background: var(--green-dim); }
.stat-number {
font-family: 'Syne', sans-serif;
font-size: 1.6rem;
font-weight: 800;
color: var(--text);
line-height: 1;
}
.stat-card.accent .stat-number { color: #818cf8; }
.stat-card.green .stat-number { color: var(--green); }
.stat-label {
font-size: 0.75rem;
color: var(--text-2);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* PROGRESS */
.progress-section { display: flex; flex-direction: column; gap: 10px; }
.progress-header {
display: flex;
justify-content: space-between;
font-size: 0.78rem;
color: var(--text-2);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.progress-bar {
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--green));
border-radius: 3px;
transition: width 0.5s ease;
}
.sidebar-footer { padding-top: 20px; border-top: 1px solid var(--border); }
.built-by {
font-size: 0.7rem;
color: var(--text-3);
font-weight: 500;
line-height: 1.8;
letter-spacing: 0.3px;
}
/* MAIN */
.main {
display: flex;
flex-direction: column;
padding: 40px 48px;
overflow-y: auto;
max-height: 100vh;
}
.main-header {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.main-header h2 {
font-family: 'Syne', sans-serif;
font-size: 1.8rem;
font-weight: 800;
color: var(--text);
letter-spacing: -0.8px;
}
.main-sub {
font-size: 0.85rem;
color: var(--text-3);
margin-top: 4px;
font-weight: 500;
}
/* INPUT */
.input-area {
display: flex;
gap: 12px;
margin-bottom: 36px;
}
input {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 20px;
font-size: 0.95rem;
font-family: 'DM Sans', sans-serif;
color: var(--text);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
input::placeholder { color: var(--text-3); }
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(99,102,241,0.12);
}
.btn-add {
background: var(--accent);
color: white;
border: none;
border-radius: 10px;
padding: 14px 28px;
font-size: 0.9rem;
font-weight: 700;
font-family: 'DM Sans', sans-serif;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
box-shadow: 0 4px 20px rgba(99,102,241,0.3);
letter-spacing: 0.2px;
}
.btn-add:hover { background: #4f46e5; transform: translateY(-1px); box-shadow: 0 8px 24px rgba(99,102,241,0.4); }
.btn-add:active { transform: translateY(0); }
.btn-add:disabled { opacity: 0.6; cursor: not-allowed; }
/* TASK GROUPS */
.task-section { display: flex; flex-direction: column; gap: 28px; }
.task-group { display: flex; flex-direction: column; gap: 8px; }
.group-label {
font-size: 0.72rem;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
padding-left: 4px;
}
.done-label { color: var(--green); }
/* TASK ITEMS */
.task-item {
display: flex;
align-items: center;
gap: 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 20px;
transition: all 0.15s;
animation: slideIn 0.2s ease;
/* ---- Focus ring ---- */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---- Animations ---- */
@keyframes slideIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
to { opacity: 1; transform: translateY(0); }
}
.task-item:hover {
border-color: var(--accent);
background: var(--surface2);
transform: translateX(3px);
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.task-item.completed {
background: var(--green-dim);
border-color: rgba(34,197,94,0.25);
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.task-item.completed:hover {
border-color: var(--green);
transform: translateX(3px);
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* CHECK BUTTON */
.check-btn {
width: 24px;
height: 24px;
border-radius: 6px;
border: 2px solid var(--text-3);
background: transparent;
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
color: transparent;
font-size: 0;
@keyframes glowPulse {
0%, 100% { box-shadow: 0 0 24px rgba(99,102,241,0.35); }
50% { box-shadow: 0 0 40px rgba(99,102,241,0.55); }
}
.check-btn:hover {
border-color: var(--green);
background: var(--green-dim);
/* ---- Utility classes ---- */
.animate-slide-in { animation: slideIn 0.2s ease; }
.animate-fade-up { animation: fadeUp 0.4s ease; }
.animate-fade-in { animation: fadeIn 0.3s ease; }
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.10s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.20s; }
.stagger-5 { animation-delay: 0.25s; }
.stagger-6 { animation-delay: 0.30s; }
.stagger-7 { animation-delay: 0.35s; }
/* ---- Progress bar ---- */
.progress-fill {
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.check-btn.checked {
background: var(--green);
border-color: var(--green);
color: white;
font-size: 0.75rem;
font-weight: 800;
/* ---- Gradient text ---- */
.gradient-text {
background: linear-gradient(135deg, #818cf8 0%, #6366f1 40%, #22c55e 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* TASK TITLE */
.task-title {
flex: 1;
font-size: 0.95rem;
color: var(--text);
font-weight: 500;
line-height: 1.4;
/* ---- Noise overlay ---- */
.noise-overlay::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
.completed .task-title {
text-decoration: line-through;
text-decoration-color: var(--green);
text-decoration-thickness: 2px;
color: var(--text-2);
/* ---- Glow pulse ---- */
.glow-pulse {
box-shadow: 0 0 24px rgba(99,102,241,0.35);
animation: glowPulse 3s ease-in-out infinite;
}
/* DELETE BUTTON */
.delete-btn {
background: var(--red-dim);
border: 1px solid rgba(239,68,68,0.3);
color: var(--red);
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
padding: 6px 14px;
border-radius: 7px;
transition: all 0.15s;
font-family: 'DM Sans', sans-serif;
flex-shrink: 0;
letter-spacing: 0.2px;
}
.delete-btn:hover {
background: var(--red);
border-color: var(--red);
color: white;
transform: scale(1.05);
}
/* EMPTY STATE */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
gap: 12px;
}
.empty-icon { font-size: 3rem; }
.empty-title {
font-family: 'Syne', sans-serif;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-2);
}
.empty-desc {
font-size: 0.85rem;
color: var(--text-3);
font-weight: 500;
}
.empty { color: var(--text-3); font-size: 0.9rem; padding: 40px 0; text-align: center; }
+154
View File
@@ -0,0 +1,154 @@
import { Link } from 'react-router-dom'
import Footer from '../components/Footer'
const VALUES = [
{ title: 'Simplicity First', description: 'We believe powerful tools do not have to be complicated. Every feature is designed to reduce friction, not add it.', icon: '✦' },
{ title: 'Built for Reliability', description: 'Downtime is not an option. Our infrastructure is designed for high availability so your team can always access their work.', icon: '⬡' },
{ title: 'Teams Over Individuals', description: 'The best work happens when teams are aligned. We build every feature with collaboration at the center.', icon: '◈' },
{ title: 'Transparency', description: 'No hidden fees, no surprise changes. We communicate openly about our product, pricing, and roadmap.', icon: '◇' },
{ title: 'Continuous Improvement', description: 'We ship improvements every week. Our changelog is public and our users drive our roadmap.', icon: '⟳' },
{ title: 'Security by Default', description: 'Security is not a feature -- it is a foundation. Every layer of our platform is built with security as a first principle.', icon: '⬟' },
]
const STATS = [
{ value: '10,000+', label: 'Tasks managed' },
{ value: '500+', label: 'Active workspaces' },
{ value: '99.9%', label: 'Uptime SLA' },
{ value: '< 200ms', label: 'API response time' },
]
export default function About() {
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</Link>
<nav className="hidden md:flex items-center gap-6">
<a href="/#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</a>
<Link to="/pricing" className="text-sm text-text2 hover:text-text1 transition-colors">Pricing</Link>
<Link to="/about" className="text-sm text-accent font-600 transition-colors">About</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className="relative overflow-hidden py-24 border-b border-border">
<div className="absolute inset-0 bg-[linear-gradient(rgba(99,102,241,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(99,102,241,0.04)_1px,transparent_1px)] bg-[size:60px_60px]" />
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-accent/8 rounded-full blur-[100px] translate-x-1/2 -translate-y-1/2 pointer-events-none" />
<div className="relative z-10 max-w-4xl mx-auto px-6 text-center">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-4">About TaskFlow</p>
<h1 className="font-display font-800 text-4xl md:text-5xl text-text1 tracking-tight leading-tight mb-6">
Built for Modern Productivity
</h1>
<p className="text-base text-text2 leading-relaxed max-w-2xl mx-auto">
Our mission is to help teams simplify workflows, improve collaboration, and scale productivity using modern cloud-powered tools designed for efficiency, reliability, and operational excellence.
</p>
</div>
</section>
{/* Stats */}
<section className="py-16 border-b border-border">
<div className="max-w-4xl mx-auto px-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{STATS.map((s, i) => (
<div key={i} className="text-center animate-fade-up" style={{ animationDelay: `${i * 0.1}s` }}>
<p className="font-display font-800 text-3xl text-accent mb-1">{s.value}</p>
<p className="text-xs text-text2 uppercase tracking-wider">{s.label}</p>
</div>
))}
</div>
</div>
</section>
{/* Mission */}
<section className="py-16 border-b border-border">
<div className="max-w-4xl mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-3">Our Mission</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-5">
Productivity tools should work as hard as you do
</h2>
<p className="text-sm text-text2 leading-relaxed mb-4">
TaskFlow was created to solve a real problem -- teams wasting time switching between tools, losing track of work, and struggling to stay aligned as they grow.
</p>
<p className="text-sm text-text2 leading-relaxed mb-4">
We built a platform that brings tasks, projects, team collaboration, and analytics into one unified workspace. No integrations required. No context switching. Just focused, efficient work.
</p>
<p className="text-sm text-text2 leading-relaxed">
TaskFlow is designed to scale with you -- from a solo developer managing personal projects to a growing team coordinating complex operations across multiple workspaces.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Founded', value: '2024' },
{ label: 'Platform', value: 'Cloud-native' },
{ label: 'Uptime', value: '99.9%' },
{ label: 'Support', value: '24/7' },
].map((item, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 text-center hover:bg-surface2 transition-colors">
<p className="font-display font-800 text-xl text-text1 mb-1">{item.value}</p>
<p className="text-xs text-text3 uppercase tracking-wider">{item.label}</p>
</div>
))}
</div>
</div>
</div>
</section>
{/* Values */}
<section className="py-16 border-b border-border">
<div className="max-w-5xl mx-auto px-6">
<div className="text-center mb-12">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-3">Our Values</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">What we stand for</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{VALUES.map((v, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200 animate-fade-up" style={{ animationDelay: `${i * 0.07}s` }}>
<div className="w-10 h-10 rounded-xl bg-accent-dim border border-accent/20 flex items-center justify-center text-accent text-lg mb-4">
{v.icon}
</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{v.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{v.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Join teams already using TaskFlow</h2>
<p className="text-sm text-text2 mb-8">Start organizing your work and collaborating with your team from one powerful workspace.</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link to="/register" className="px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Free
</Link>
<Link to="/contact" className="px-8 py-3.5 bg-surface hover:bg-surface2 text-text1 font-600 rounded-xl border border-border text-sm transition-all">
Contact Us
</Link>
</div>
</div>
</section>
<Footer />
</div>
)
}
+127
View File
@@ -0,0 +1,127 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
function getToken() { return localStorage.getItem('token') }
function timeAgo(date) {
const seconds = Math.floor((new Date() - new Date(date)) / 1000)
if (seconds < 60) return 'just now'
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
return `${Math.floor(seconds / 86400)}d ago`
}
function ActivityIcon({ type }) {
if (type === 'completed') return (
<div className="w-8 h-8 rounded-full bg-green-dim border border-green/30 flex items-center justify-center flex-shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</div>
)
if (type === 'created') return (
<div className="w-8 h-8 rounded-full bg-accent-dim border border-accent/30 flex items-center justify-center flex-shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
)
if (type === 'deleted') return (
<div className="w-8 h-8 rounded-full bg-red-dim border border-red/30 flex items-center justify-center flex-shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
</div>
)
return (
<div className="w-8 h-8 rounded-full bg-surface2 border border-border flex items-center justify-center flex-shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="2"><circle cx="12" cy="12" r="10"/></svg>
</div>
)
}
export default function Activity() {
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/tasks', { headers: { Authorization: `Bearer ${getToken()}` } })
.then(r => r.json())
.then(d => setTasks(Array.isArray(d) ? d : []))
.catch(() => setTasks([]))
.finally(() => setLoading(false))
}, [])
// Build activity feed from tasks
const activities = tasks.flatMap(task => {
const events = []
events.push({ id: `${task.id}-created`, type: 'created', title: task.title, date: task.created_at })
if (task.completed) {
events.push({ id: `${task.id}-completed`, type: 'completed', title: task.title, date: task.created_at })
}
return events
}).sort((a, b) => new Date(b.date) - new Date(a.date))
const ACTION_TEXT = {
created: 'created task',
completed: 'completed task',
deleted: 'deleted task',
updated: 'updated task',
}
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-2xl mx-auto px-8 py-10">
<div className="mb-8">
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Activity</h1>
<p className="text-sm text-text2 mt-1">A log of everything that has happened in your workspace.</p>
</div>
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{!loading && activities.length === 0 && (
<div className="flex flex-col items-center py-20 gap-3 text-center">
<div className="w-14 h-14 rounded-2xl bg-surface2 border border-border flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3d4257" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<p className="font-display font-700 text-text2">No activity yet</p>
<p className="text-sm text-text3">Start creating and completing tasks to see your activity here.</p>
</div>
)}
{!loading && activities.length > 0 && (
<div className="relative">
{/* Timeline line */}
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
<div className="flex flex-col gap-1">
{activities.map((activity, i) => (
<div key={activity.id} className="flex items-start gap-4 pl-0 animate-fade-up" style={{ animationDelay: `${i * 0.03}s` }}>
<div className="relative z-10 flex-shrink-0">
<ActivityIcon type={activity.type} />
</div>
<div className="flex-1 bg-surface border border-border rounded-xl px-4 py-3 mb-3 hover:bg-surface2 transition-colors">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs text-text3 mb-0.5 uppercase tracking-wider font-600">
{ACTION_TEXT[activity.type] || activity.type}
</p>
<p className="text-sm text-text1 font-500 leading-snug">{activity.title}</p>
</div>
<span className="text-[0.65rem] text-text3 flex-shrink-0 mt-0.5">{timeAgo(activity.date)}</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</main>
</div>
)
}
+188
View File
@@ -0,0 +1,188 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer,
PieChart, Pie, Cell, LineChart, Line, CartesianGrid, Legend,
} from 'recharts'
function getToken() { return localStorage.getItem('token') }
const CHART_COLORS = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
function StatCard({ label, value, sub, accent, green }) {
return (
<div className={`rounded-xl border p-5 ${accent ? 'bg-accent-dim border-accent/25' : green ? 'bg-green-dim border-green/25' : 'bg-surface border-border'}`}>
<p className={`font-display font-800 text-3xl leading-none mb-1 ${accent ? 'text-[#818cf8]' : green ? 'text-green' : 'text-text1'}`}>{value}</p>
<p className="text-xs font-600 text-text2 uppercase tracking-wider">{label}</p>
{sub && <p className="text-xs text-text3 mt-1">{sub}</p>}
</div>
)
}
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null
return (
<div className="bg-surface2 border border-border rounded-lg px-3 py-2 text-xs shadow-lg">
<p className="text-text2 mb-1">{label}</p>
{payload.map((p, i) => (
<p key={i} style={{ color: p.color }} className="font-600">{p.name}: {p.value}</p>
))}
</div>
)
}
export default function Analytics() {
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/tasks', { headers: { Authorization: `Bearer ${getToken()}` } })
.then(r => r.json())
.then(d => setTasks(Array.isArray(d) ? d : []))
.catch(() => setTasks([]))
.finally(() => setLoading(false))
}, [])
const total = tasks.length
const completed = tasks.filter(t => t.completed).length
const active = tasks.filter(t => !t.completed).length
const rate = total > 0 ? Math.round((completed / total) * 100) : 0
// Tasks created per day (last 7 days)
const last7 = [...Array(7)].map((_, i) => {
const d = new Date()
d.setDate(d.getDate() - (6 - i))
const key = d.toDateString()
const label = d.toLocaleDateString('en-US', { weekday: 'short' })
const created = tasks.filter(t => new Date(t.created_at).toDateString() === key).length
const done = tasks.filter(t => t.completed && new Date(t.created_at).toDateString() === key).length
return { day: label, Created: created, Completed: done }
})
// Pie chart data
const pieData = [
{ name: 'Completed', value: completed || 0 },
{ name: 'Active', value: active || 0 },
]
// Task creation trend (last 4 weeks)
const weeklyData = [...Array(4)].map((_, i) => {
const weekStart = new Date()
weekStart.setDate(weekStart.getDate() - (3 - i) * 7 - weekStart.getDay())
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekEnd.getDate() + 6)
const count = tasks.filter(t => {
const d = new Date(t.created_at)
return d >= weekStart && d <= weekEnd
}).length
return { week: `W${i + 1}`, Tasks: count }
})
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-5xl mx-auto px-8 py-10">
<div className="mb-8">
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Analytics</h1>
<p className="text-sm text-text2 mt-1">Track your productivity and task completion over time.</p>
</div>
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{!loading && (
<>
{/* Stats row */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<StatCard label="Total Tasks" value={total} />
<StatCard label="Completed" value={completed} green />
<StatCard label="Active" value={active} accent />
<StatCard label="Completion Rate" value={`${rate}%`} sub={rate >= 50 ? 'Great work!' : 'Keep going!'} />
</div>
{/* Charts row 1 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4">
{/* Bar chart - daily activity */}
<div className="lg:col-span-2 bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-4">Daily Activity (Last 7 Days)</h3>
{total === 0 ? (
<div className="flex items-center justify-center h-40 text-sm text-text3">No task data yet</div>
) : (
<ResponsiveContainer width="100%" height={180}>
<BarChart data={last7} barSize={12} barGap={4}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2028" vertical={false} />
<XAxis dataKey="day" tick={{ fill: '#94a3b8', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} />
<Bar dataKey="Created" fill="#6366f1" radius={[4, 4, 0, 0]} />
<Bar dataKey="Completed" fill="#22c55e" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
</div>
{/* Pie chart - completion breakdown */}
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-4">Task Breakdown</h3>
{total === 0 ? (
<div className="flex items-center justify-center h-40 text-sm text-text3">No task data yet</div>
) : (
<>
<ResponsiveContainer width="100%" height={140}>
<PieChart>
<Pie data={pieData} cx="50%" cy="50%" innerRadius={40} outerRadius={65} paddingAngle={3} dataKey="value">
{pieData.map((_, i) => (
<Cell key={i} fill={CHART_COLORS[i]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
<div className="flex flex-col gap-2 mt-2">
{pieData.map((item, i) => (
<div key={item.name} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ background: CHART_COLORS[i] }} />
<span className="text-text2">{item.name}</span>
</div>
<span className="font-600 text-text1">{item.value}</span>
</div>
))}
</div>
</>
)}
</div>
</div>
{/* Line chart - weekly trend */}
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-4">Weekly Task Creation Trend</h3>
{total === 0 ? (
<div className="flex items-center justify-center h-40 text-sm text-text3">No task data yet</div>
) : (
<ResponsiveContainer width="100%" height={160}>
<LineChart data={weeklyData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2028" vertical={false} />
<XAxis dataKey="week" tick={{ fill: '#94a3b8', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="Tasks" stroke="#6366f1" strokeWidth={2.5} dot={{ fill: '#6366f1', r: 4 }} activeDot={{ r: 6 }} />
</LineChart>
</ResponsiveContainer>
)}
</div>
</>
)}
</div>
</main>
</div>
)
}
+146
View File
@@ -0,0 +1,146 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
function getToken() { return localStorage.getItem('token') }
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']
export default function Calendar() {
const [tasks, setTasks] = useState([])
const [today] = useState(new Date())
const [current, setCurrent] = useState(new Date())
useEffect(() => {
fetch('/api/tasks', { headers: { Authorization: `Bearer ${getToken()}` } })
.then(r => r.json())
.then(d => setTasks(Array.isArray(d) ? d : []))
.catch(() => setTasks([]))
}, [])
const year = current.getFullYear()
const month = current.getMonth()
const firstDay = new Date(year, month, 1).getDay()
const daysInMonth = new Date(year, month + 1, 0).getDate()
const prevMonth = () => setCurrent(new Date(year, month - 1, 1))
const nextMonth = () => setCurrent(new Date(year, month + 1, 1))
// Group tasks by due_date date string
const tasksByDate = tasks.reduce((acc, task) => {
if (!task.due_date) return acc
const key = new Date(task.due_date).toDateString()
if (!acc[key]) acc[key] = []
acc[key].push(task)
return acc
}, {})
// Tasks with no due date
const noDueDate = tasks.filter(t => !t.due_date && !t.completed)
const cells = []
for (let i = 0; i < firstDay; i++) cells.push(null)
for (let d = 1; d <= daysInMonth; d++) cells.push(d)
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-5xl mx-auto px-8 py-10">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Calendar</h1>
<p className="text-sm text-text2 mt-1">View your tasks and deadlines by date.</p>
</div>
<div className="flex items-center gap-2">
<button onClick={prevMonth} className="p-2 rounded-lg bg-surface border border-border text-text2 hover:text-text1 hover:bg-surface2 transition-all">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<span className="font-display font-700 text-text1 text-sm px-3 min-w-[140px] text-center">
{MONTHS[month]} {year}
</span>
<button onClick={nextMonth} className="p-2 rounded-lg bg-surface border border-border text-text2 hover:text-text1 hover:bg-surface2 transition-all">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
</div>
{/* Calendar grid */}
<div className="bg-surface border border-border rounded-2xl overflow-hidden mb-6">
{/* Day headers */}
<div className="grid grid-cols-7 border-b border-border">
{DAYS.map(d => (
<div key={d} className="px-3 py-3 text-center text-[0.7rem] font-700 text-text3 uppercase tracking-wider">
{d}
</div>
))}
</div>
{/* Cells */}
<div className="grid grid-cols-7">
{cells.map((day, i) => {
if (!day) return <div key={`empty-${i}`} className="min-h-[100px] border-b border-r border-border bg-surface2/30" />
const dateObj = new Date(year, month, day)
const isToday = dateObj.toDateString() === today.toDateString()
const isPast = dateObj < today && !isToday
const dayTasks = tasksByDate[dateObj.toDateString()] || []
const isLastRow = i >= cells.length - 7
const isLastCol = (i + 1) % 7 === 0
return (
<div
key={day}
className={`min-h-[100px] p-2 border-border
${!isLastRow ? 'border-b' : ''}
${!isLastCol ? 'border-r' : ''}
${isToday ? 'bg-accent-dim' : isPast ? 'bg-surface2/20' : ''}
transition-colors`}
>
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-700 mb-1
${isToday ? 'bg-accent text-white' : isPast ? 'text-text3' : 'text-text2 hover:bg-surface2'}`}>
{day}
</div>
<div className="flex flex-col gap-1">
{dayTasks.slice(0, 3).map(task => (
<div
key={task.id}
className={`text-[0.6rem] font-500 px-1.5 py-0.5 rounded truncate
${task.completed ? 'bg-green-dim text-green line-through' : 'bg-accent-dim text-accent'}`}
>
{task.title}
</div>
))}
{dayTasks.length > 3 && (
<div className="text-[0.6rem] text-text3 px-1">+{dayTasks.length - 3} more</div>
)}
</div>
</div>
)
})}
</div>
</div>
{/* Unscheduled tasks */}
{noDueDate.length > 0 && (
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="text-xs font-700 text-text3 uppercase tracking-wider mb-3">Unscheduled Tasks</h3>
<div className="flex flex-col gap-2">
{noDueDate.map(task => (
<div key={task.id} className="flex items-center gap-3 px-3 py-2.5 bg-surface2 rounded-lg border border-border">
<div className="w-1.5 h-1.5 rounded-full bg-text3 flex-shrink-0" />
<span className="text-sm text-text2">{task.title}</span>
</div>
))}
</div>
</div>
)}
</div>
</main>
</div>
)
}
+230
View File
@@ -0,0 +1,230 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import Footer from '../components/Footer'
export default function Contact() {
const [form, setForm] = useState({ name: '', email: '', subject: '', message: '' })
const [sent, setSent] = useState(false)
const [sending, setSending] = useState(false)
const handleSubmit = e => {
e.preventDefault()
setSending(true)
// Simulate sending
setTimeout(() => {
setSending(false)
setSent(true)
}, 1200)
}
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</Link>
<nav className="hidden md:flex items-center gap-6">
<a href="/#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</a>
<Link to="/pricing" className="text-sm text-text2 hover:text-text1 transition-colors">Pricing</Link>
<Link to="/about" className="text-sm text-text2 hover:text-text1 transition-colors">About</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className="py-20 border-b border-border text-center">
<div className="max-w-2xl mx-auto px-6">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-3">Contact</p>
<h1 className="font-display font-800 text-4xl text-text1 tracking-tight mb-4">Get in Touch</h1>
<p className="text-sm text-text2 leading-relaxed">
Have a question, feedback, or want to discuss how TaskFlow can work for your team? We would love to hear from you.
</p>
</div>
</section>
{/* Content */}
<section className="py-16 flex-1">
<div className="max-w-5xl mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-10">
{/* Contact info */}
<div className="lg:col-span-2 flex flex-col gap-5">
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-4">Contact Information</h3>
<div className="flex flex-col gap-4">
<a
href="mailto:elizabethik135@gmail.com"
className="flex items-center gap-3 text-sm text-text2 hover:text-accent transition-colors group"
>
<div className="w-8 h-8 rounded-lg bg-accent-dim border border-accent/20 flex items-center justify-center flex-shrink-0 group-hover:bg-accent group-hover:border-accent transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent group-hover:text-white">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</div>
elizabethik135@gmail.com
</a>
<a
href="https://www.linkedin.com/in/ikechukwu-elizabeth"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 text-sm text-text2 hover:text-accent transition-colors group"
>
<div className="w-8 h-8 rounded-lg bg-accent-dim border border-accent/20 flex items-center justify-center flex-shrink-0 group-hover:bg-accent group-hover:border-accent transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-accent group-hover:text-white">
<path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2z"/>
<circle cx="4" cy="4" r="2"/>
</svg>
</div>
LinkedIn Profile
</a>
<a
href="https://github.com/elizabeth-ikechukwu/scalable-task-platform"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 text-sm text-text2 hover:text-accent transition-colors group"
>
<div className="w-8 h-8 rounded-lg bg-accent-dim border border-accent/20 flex items-center justify-center flex-shrink-0 group-hover:bg-accent group-hover:border-accent transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-accent group-hover:text-white">
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
</svg>
</div>
GitHub Repository
</a>
</div>
</div>
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-3">Response Time</h3>
<p className="text-xs text-text2 leading-relaxed">
We typically respond within 24 hours on business days. For urgent issues, email is the fastest way to reach us.
</p>
</div>
<div className="bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-sm mb-3">Open Source</h3>
<p className="text-xs text-text2 leading-relaxed mb-3">
TaskFlow is open source. Found a bug or want to contribute? Open an issue or pull request on GitHub.
</p>
<a
href="https://github.com/elizabeth-ikechukwu/scalable-task-platform"
target="_blank"
rel="noopener noreferrer"
className="text-xs font-600 text-accent hover:underline"
>
View on GitHub →
</a>
</div>
</div>
{/* Contact form */}
<div className="lg:col-span-3">
<div className="bg-surface border border-border rounded-2xl p-6">
{sent ? (
<div className="flex flex-col items-center justify-center py-16 text-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-green-dim border border-green/30 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<h3 className="font-display font-700 text-text1 text-lg">Message Sent!</h3>
<p className="text-sm text-text2 max-w-xs">Thank you for reaching out. We will get back to you within 24 hours.</p>
<button
onClick={() => { setSent(false); setForm({ name: '', email: '', subject: '', message: '' }) }}
className="mt-2 text-xs font-600 text-accent hover:underline"
>
Send another message
</button>
</div>
) : (
<>
<h2 className="font-display font-700 text-text1 text-lg mb-5">Send a Message</h2>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Full Name</label>
<input
value={form.name}
onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
placeholder="Your name"
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Email Address</label>
<input
type="email"
value={form.email}
onChange={e => setForm(p => ({ ...p, email: e.target.value }))}
placeholder="you@example.com"
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Subject</label>
<select
value={form.subject}
onChange={e => setForm(p => ({ ...p, subject: e.target.value }))}
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 outline-none focus:border-accent transition-all"
>
<option value="">Select a subject</option>
<option value="general">General Inquiry</option>
<option value="support">Technical Support</option>
<option value="billing">Billing Question</option>
<option value="enterprise">Enterprise Sales</option>
<option value="feedback">Product Feedback</option>
</select>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Message</label>
<textarea
value={form.message}
onChange={e => setForm(p => ({ ...p, message: e.target.value }))}
placeholder="Tell us how we can help..."
rows={5}
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all resize-none"
/>
</div>
<button
type="submit"
disabled={sending}
className="w-full py-3 bg-accent hover:bg-accent-hover disabled:opacity-60 text-white font-700 rounded-lg text-sm transition-all shadow-accent hover:-translate-y-0.5 flex items-center justify-center gap-2"
>
{sending ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Sending...
</>
) : 'Send Message'}
</button>
</form>
</>
)}
</div>
</div>
</div>
</div>
</section>
<Footer />
</div>
)
}
+286
View File
@@ -0,0 +1,286 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import { EmptyTasks } from '../components/EmptyState'
const API = '/api'
/* ---- Sub-components ---- */
function StatCard({ value, label, accent, green }) {
return (
<div className={`
rounded-xl border p-5 flex flex-col gap-1 transition-colors duration-150
${accent ? 'bg-accent-dim border-accent/25' : green ? 'bg-green-dim border-green/25' : 'bg-surface2 border-border hover:border-text3'}
`}>
<span className={`font-display font-800 text-3xl leading-none ${accent ? 'text-[#818cf8]' : green ? 'text-green' : 'text-text1'}`}>
{value}
</span>
<span className="text-xs font-500 text-text2 uppercase tracking-wider">{label}</span>
</div>
)
}
function ProgressBar({ value }) {
return (
<div className="rounded-xl border border-border bg-surface2 p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-700 text-text2 uppercase tracking-wider">Overall Progress</span>
<span className="text-sm font-700 text-accent">{Math.round(value)}%</span>
</div>
<div className="h-2 bg-border rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent to-green rounded-full progress-fill"
style={{ width: `${value}%` }}
/>
</div>
<p className="mt-2 text-xs text-text3">
{value === 100 ? 'All tasks complete!' : value === 0 ? 'No tasks completed yet' : 'Keep going, you\'re making progress'}
</p>
</div>
)
}
function TaskItem({ task, onToggle, onDelete }) {
return (
<div className={`
flex items-center gap-3 rounded-xl border px-4 py-3.5 transition-all duration-150 animate-slide-in
${task.completed
? 'bg-green-dim border-green/20 hover:border-green/40'
: 'bg-surface border-border hover:border-accent/40 hover:bg-surface2 hover:translate-x-0.5'
}
`}>
{/* Check button */}
<button
onClick={() => onToggle(task.id)}
title={task.completed ? 'Mark incomplete' : 'Mark complete'}
className={`
w-5 h-5 rounded-md border-2 flex items-center justify-center flex-shrink-0 transition-all duration-150
${task.completed
? 'bg-green border-green'
: 'border-text3 hover:border-green hover:bg-green-dim'
}
`}
>
{task.completed && (
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2.5">
<polyline points="1.5,6 5,9.5 10.5,2.5"/>
</svg>
)}
</button>
{/* Title */}
<span className={`flex-1 text-sm font-500 leading-snug ${task.completed ? 'line-through text-text3 decoration-green decoration-2' : 'text-text1'}`}>
{task.title}
</span>
{/* Delete button */}
<button
onClick={() => onDelete(task.id)}
className="flex-shrink-0 px-2.5 py-1 rounded-md text-xs font-600 text-red bg-red-dim border border-red/20 hover:bg-red hover:text-white hover:border-red transition-all duration-150"
>
Delete
</button>
</div>
)
}
/* ---- Dashboard page ---- */
export default function Dashboard() {
const [tasks, setTasks] = useState([])
const [title, setTitle] = useState('')
const [loading, setLoading] = useState(true)
const [adding, setAdding] = useState(false)
const [inputFocused, setInputFocused] = useState(false)
const fetchTasks = async () => {
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API}/tasks`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
const data = await res.json()
setTasks(Array.isArray(data) ? data : [])
} catch {
setTasks([])
} finally {
setLoading(false)
}
}
useEffect(() => { fetchTasks() }, [])
const addTask = async () => {
if (!title.trim()) return
setAdding(true)
try {
const token = localStorage.getItem('token')
await fetch(`${API}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ title }),
})
setTitle('')
fetchTasks()
} finally {
setAdding(false)
}
}
const toggleTask = async id => {
const token = localStorage.getItem('token')
await fetch(`${API}/tasks/${id}`, {
method: 'PATCH',
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
fetchTasks()
}
const deleteTask = async id => {
const token = localStorage.getItem('token')
await fetch(`${API}/tasks/${id}`, {
method: 'DELETE',
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
fetchTasks()
}
const completed = tasks.filter(t => t.completed).length
const remaining = tasks.filter(t => !t.completed).length
const progress = tasks.length > 0 ? (completed / tasks.length) * 100 : 0
const now = new Date()
const hour = now.getHours()
const greeting = hour < 12 ? 'Good morning' : hour < 17 ? 'Good afternoon' : 'Good evening'
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
{/* Main content - offset by sidebar width */}
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-4xl mx-auto px-8 py-10">
{/* ---- Welcome card ---- */}
<div className="relative rounded-2xl border border-border bg-surface overflow-hidden mb-8 p-7">
{/* Background glow */}
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/8 rounded-full blur-[80px] translate-x-1/3 -translate-y-1/3 pointer-events-none" />
<div className="relative z-10">
<p className="text-xs font-700 text-text3 uppercase tracking-widest mb-1">
{now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</p>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight mb-1">
{greeting} <span className="animate-fade-in">&#128075;</span>
</h1>
<p className="text-sm text-text2 font-500">Stay focused, organized, and in control of your work.</p>
<p className="text-xs text-text3 mt-2 max-w-xl leading-relaxed">
Track active projects, manage deadlines, collaborate with your team, and monitor progress from a single dashboard built for productivity at scale.
</p>
</div>
</div>
{/* ---- Stats row ---- */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard value={tasks.length} label="Total Tasks" />
<StatCard value={remaining} label="Remaining" accent />
<StatCard value={completed} label="Completed" green />
<StatCard value={`${Math.round(progress)}%`} label="Progress" />
</div>
{/* ---- Progress bar ---- */}
<div className="mb-8">
<ProgressBar value={progress} />
</div>
{/* ---- Tasks panel ---- */}
<div className="rounded-2xl border border-border bg-surface overflow-hidden">
<div className="px-6 py-5 border-b border-border flex items-center justify-between">
<div>
<h2 className="font-display font-700 text-text1 tracking-tight">My Tasks</h2>
<p className="text-xs text-text3 mt-0.5">Stay focused. Ship things.</p>
</div>
<span className="text-xs font-600 text-text3 bg-surface2 border border-border rounded-full px-3 py-1">
{tasks.length} total
</span>
</div>
<div className="p-6">
{/* Input */}
<div className={`flex gap-3 mb-6 p-1 rounded-xl border transition-all duration-150 ${inputFocused ? 'border-accent bg-surface2 shadow-[0_0_0_3px_rgba(99,102,241,0.1)]' : 'border-border bg-surface2'}`}>
<input
value={title}
onChange={e => setTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTask()}
onFocus={() => setInputFocused(true)}
onBlur={() => setInputFocused(false)}
placeholder="What needs to be done? Press Enter to add..."
className="flex-1 bg-transparent px-3 py-2.5 text-sm text-text1 placeholder-text3 outline-none"
/>
<button
onClick={addTask}
disabled={adding || !title.trim()}
className="px-5 py-2.5 bg-accent hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-700 rounded-lg transition-all duration-150 whitespace-nowrap flex-shrink-0"
>
{adding ? '...' : '+ Add Task'}
</button>
</div>
{/* Task list */}
{loading && (
<div className="flex flex-col items-center py-16 gap-3">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
<p className="text-xs text-text3">Loading your tasks...</p>
</div>
)}
{!loading && tasks.length === 0 && (
<EmptyTasks onAdd={() => document.querySelector('input')?.focus()} />
)}
{!loading && tasks.length > 0 && (
<div className="flex flex-col gap-6">
{/* Active tasks */}
{tasks.filter(t => !t.completed).length > 0 && (
<div>
<p className="text-[0.7rem] font-700 text-accent uppercase tracking-widest mb-2 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-accent inline-block" />
Active ({tasks.filter(t => !t.completed).length})
</p>
<div className="flex flex-col gap-2">
{tasks.filter(t => !t.completed).map(task => (
<TaskItem key={task.id} task={task} onToggle={toggleTask} onDelete={deleteTask} />
))}
</div>
</div>
)}
{/* Completed tasks */}
{tasks.filter(t => t.completed).length > 0 && (
<div>
<p className="text-[0.7rem] font-700 text-green uppercase tracking-widest mb-2 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-green inline-block" />
Completed ({tasks.filter(t => t.completed).length})
</p>
<div className="flex flex-col gap-2">
{tasks.filter(t => t.completed).map(task => (
<TaskItem key={task.id} task={task} onToggle={toggleTask} onDelete={deleteTask} />
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Bottom metadata */}
<p className="mt-6 text-center text-xs text-text3">
Node.js - Express - PostgreSQL on AWS RDS - Docker - Nginx - Terraform
</p>
</div>
</main>
</div>
)
}
+248
View File
@@ -0,0 +1,248 @@
import { Link } from 'react-router-dom'
import Footer from '../components/Footer'
import FeatureCard from '../components/FeatureCard'
const FEATURES = [
{
title: 'Task Management',
description: 'Create, assign, and prioritize tasks with due dates, labels, and status tracking. Keep every piece of work accounted for.',
to: '/features/task-management',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
),
color: 'text-accent',
bg: 'bg-accent-dim border-accent/20',
},
{
title: 'Team Collaboration',
description: 'Work alongside your team in shared workspaces. Assign tasks, leave comments, and stay aligned without switching tools.',
to: '/features/team-collaboration',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
</svg>
),
color: 'text-green',
bg: 'bg-green-dim border-green/20',
},
{
title: 'Smart Notifications',
description: 'Stay informed with context-aware alerts. Get notified about deadlines, mentions, and updates that actually matter to you.',
to: '/features/notifications',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 01-3.46 0"/>
</svg>
),
color: 'text-yellow-400',
bg: 'bg-yellow-400/10 border-yellow-400/20',
},
{
title: 'Project Workspaces',
description: 'Organize tasks into focused project spaces. Each workspace gives your team a dedicated area to plan, execute, and review.',
to: '/features/project-workspaces',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 7a2 2 0 012-2h4l2 3h9a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V7z"/>
</svg>
),
color: 'text-blue-400',
bg: 'bg-blue-400/10 border-blue-400/20',
},
{
title: 'Analytics & Reporting',
description: 'Track velocity, completion rates, and team throughput with visual dashboards. Turn raw activity into actionable insights.',
to: '/features/analytics',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
),
color: 'text-purple-400',
bg: 'bg-purple-400/10 border-purple-400/20',
},
{
title: 'Secure Access Control',
description: 'JWT-based authentication, role-based permissions, and encrypted credentials. Your data stays private by design.',
to: '/features/security',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0110 0v4"/>
</svg>
),
color: 'text-red-400',
bg: 'bg-red-400/10 border-red-400/20',
},
{
title: 'Scalable Architecture',
description: 'Cloud-native infrastructure designed to grow with your team. Built for reliability, performance, and operational excellence.',
to: '/features/architecture',
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
),
color: 'text-accent',
bg: 'bg-accent-dim border-accent/20',
},
]
function DashboardPreview() {
return (
<div className="relative w-full max-w-2xl mx-auto">
<div className="absolute inset-0 bg-accent/10 blur-3xl rounded-3xl scale-95" />
<div className="relative bg-surface border border-border rounded-2xl overflow-hidden shadow-2xl">
<div className="flex items-center gap-2 px-4 py-3 border-b border-border bg-surface2">
<span className="w-2.5 h-2.5 rounded-full bg-red/60" />
<span className="w-2.5 h-2.5 rounded-full bg-yellow-400/60" />
<span className="w-2.5 h-2.5 rounded-full bg-green/60" />
<span className="ml-3 text-xs text-text3 font-500">app.taskflow.io/dashboard</span>
</div>
<div className="flex h-52">
<div className="w-14 bg-surface2 border-r border-border flex flex-col items-center py-3 gap-3">
{[...Array(5)].map((_, i) => (
<div key={i} className={`w-7 h-7 rounded-lg ${i === 0 ? 'bg-accent' : 'bg-border'}`} />
))}
</div>
<div className="flex-1 p-4 flex flex-col gap-3">
<div className="grid grid-cols-3 gap-2">
{['24 Tasks', '18 Done', '6 Active'].map((s, i) => (
<div key={i} className="bg-surface2 rounded-lg p-2 border border-border">
<div className="text-xs font-700 text-text1">{s.split(' ')[0]}</div>
<div className="text-[0.6rem] text-text3">{s.split(' ')[1]}</div>
</div>
))}
</div>
<div className="flex flex-col gap-1.5">
{[
{ label: 'Review Q3 product roadmap', done: true },
{ label: 'Design onboarding flow', done: true },
{ label: 'Launch email campaign', done: false },
{ label: 'Prepare team performance review', done: false },
].map((t, i) => (
<div key={i} className="flex items-center gap-2 bg-surface2 rounded-md px-2.5 py-1.5 border border-border">
<div className={`w-3.5 h-3.5 rounded flex items-center justify-center flex-shrink-0 ${t.done ? 'bg-green border-green' : 'border border-text3'}`}>
{t.done && <svg width="8" height="8" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>}
</div>
<span className={`text-[0.65rem] ${t.done ? 'line-through text-text3' : 'text-text2'}`}>{t.label}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
export default function Landing() {
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</div>
<nav className="hidden md:flex items-center gap-6">
<a href="#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</a>
<Link to="/pricing" className="text-sm text-text2 hover:text-text1 transition-colors">Pricing</Link>
<Link to="/about" className="text-sm text-text2 hover:text-text1 transition-colors">About</Link>
<Link to="/dashboard" className="text-sm text-text2 hover:text-text1 transition-colors">Dashboard</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className="relative overflow-hidden flex-1 flex items-center noise-overlay">
<div className="absolute inset-0 bg-[linear-gradient(rgba(99,102,241,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(99,102,241,0.04)_1px,transparent_1px)] bg-[size:60px_60px]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-accent/8 rounded-full blur-[120px] pointer-events-none" />
<div className="relative z-10 max-w-6xl mx-auto px-6 py-24 w-full">
<div className="flex flex-col items-center text-center mb-16">
<h1 className="font-display font-800 text-4xl md:text-6xl text-text1 leading-[1.1] tracking-tight mb-6 max-w-3xl animate-fade-up">
Manage Tasks, Teams, and{' '}
<span className="gradient-text">Workflows at Scale</span>
</h1>
<p className="text-base md:text-lg text-text2 leading-relaxed max-w-2xl mb-10 animate-fade-up stagger-2">
A modern task management platform built for individuals, startups, and growing teams to organize projects, automate workflows, track progress, and collaborate in real time -- all from one centralized workspace.
</p>
<div className="flex flex-col sm:flex-row items-center gap-4 animate-fade-up stagger-3">
<Link to="/register"
className="w-full sm:w-auto px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl transition-all shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5 text-sm tracking-wide">
Get Started Free
</Link>
<Link to="/dashboard"
className="w-full sm:w-auto px-8 py-3.5 bg-surface hover:bg-surface2 text-text1 font-600 rounded-xl border border-border hover:border-text3 transition-all text-sm tracking-wide flex items-center justify-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/>
</svg>
View Demo
</Link>
</div>
</div>
<div className="animate-fade-up stagger-4">
<DashboardPreview />
</div>
</div>
</section>
{/* Features */}
<section id="features" className="py-24 border-t border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-16">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-3">Platform Features</p>
<h2 className="font-display font-800 text-3xl md:text-4xl text-text1 tracking-tight mb-4">
Everything your team needs
</h2>
<p className="text-text2 max-w-xl mx-auto text-sm leading-relaxed">
Click any feature to learn more about how it works and what it includes.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{FEATURES.map((f, i) => (
<FeatureCard key={f.title} {...f} index={i} />
))}
</div>
</div>
</section>
{/* CTA banner */}
<section className="py-20 border-t border-border">
<div className="max-w-3xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-3xl text-text1 tracking-tight mb-4">Ready to get organized?</h2>
<p className="text-text2 text-sm leading-relaxed mb-8 max-w-lg mx-auto">
Join thousands of teams managing tasks, projects, and workflows from one powerful workspace.
</p>
<Link to="/register"
className="inline-block px-10 py-4 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl transition-all shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5">
Start for Free
</Link>
</div>
</section>
<Footer />
</div>
)
}
+165
View File
@@ -0,0 +1,165 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
const API = '/api'
export default function Login() {
const navigate = useNavigate()
const [form, setForm] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
const handleSubmit = async e => {
e.preventDefault()
setError('')
setLoading(true)
try {
const res = await fetch(`${API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
const data = await res.json()
if (!res.ok) { setError(data.error || 'Login failed'); return }
localStorage.setItem('token', data.token)
navigate('/dashboard')
} catch {
setError('Network error. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-bg flex">
{/* ---- Left branding panel ---- */}
<div className="hidden lg:flex lg:w-1/2 relative flex-col justify-between p-12 overflow-hidden">
{/* Background layers */}
<div className="absolute inset-0 bg-gradient-to-br from-accent/20 via-bg to-bg" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(99,102,241,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(99,102,241,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-accent/10 rounded-full blur-[100px] -translate-x-1/2 translate-y-1/2" />
{/* Content */}
<div className="relative z-10">
<Link to="/" className="flex items-center gap-2.5 group">
<div className="w-9 h-9 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 text-lg tracking-tight">TaskFlow</span>
</Link>
</div>
<div className="relative z-10">
<h2 className="font-display font-800 text-3xl text-text1 leading-tight tracking-tight mb-4">
Access your workspace and continue managing your projects efficiently.
</h2>
<p className="text-text2 text-sm leading-relaxed max-w-sm">
Organized tasks, focused teams, and infrastructure that scales. Everything in one place.
</p>
{/* Trust indicators */}
<div className="mt-10 flex flex-col gap-3">
{[
'PostgreSQL on AWS RDS',
'JWT-secured authentication',
'CI/CD via GitHub Actions',
].map(item => (
<div key={item} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-green-dim border border-green/30 flex items-center justify-center flex-shrink-0">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>
</div>
<span className="text-sm text-text2">{item}</span>
</div>
))}
</div>
</div>
<div className="relative z-10">
<p className="text-xs text-text3">
Built by{' '}
<a href="https://www.linkedin.com/in/ikechukwu-elizabeth" target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">
Elizabeth Ikechukwu
</a>
{' '}- DevOps &amp; Cloud Engineer
</p>
</div>
</div>
{/* ---- Right auth panel ---- */}
<div className="flex-1 flex items-center justify-center px-6 py-16">
<div className="w-full max-w-sm">
{/* Mobile logo */}
<div className="flex items-center gap-2.5 mb-10 lg:hidden">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1">TaskFlow</span>
</div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight mb-1">Welcome back</h1>
<p className="text-sm text-text2 mb-8">Sign in to your workspace to continue.</p>
{error && (
<div className="mb-5 px-4 py-3 rounded-lg bg-red-dim border border-red/30 text-red text-sm flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Email address</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="you@example.com"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs font-600 text-text2 uppercase tracking-wider">Password</label>
<a href="#" className="text-xs text-accent hover:underline">Forgot password?</a>
</div>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="Enter your password"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full mt-2 py-3 bg-accent hover:bg-accent-hover disabled:opacity-60 disabled:cursor-not-allowed text-white font-700 rounded-lg text-sm transition-all duration-150 shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5 active:translate-y-0"
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
<p className="mt-6 text-center text-sm text-text2">
Don&apos;t have an account?{' '}
<Link to="/register" className="text-accent hover:underline font-600">Create one free</Link>
</p>
</div>
</div>
</div>
)
}
+192
View File
@@ -0,0 +1,192 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import { EmptyNotifications } from '../components/EmptyState'
function getToken() { return localStorage.getItem('token') }
// Notification types and their styles
const TYPE_CONFIG = {
deadline: {
color: 'text-yellow-400',
bg: 'bg-yellow-400/10 border-yellow-400/20',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
),
},
completed: {
color: 'text-green',
bg: 'bg-green-dim border-green/20',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
},
mention: {
color: 'text-accent',
bg: 'bg-accent-dim border-accent/20',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4"/>
<path d="M16 8v5a3 3 0 006 0v-1a10 10 0 10-3.92 7.94"/>
</svg>
),
},
system: {
color: 'text-text2',
bg: 'bg-surface2 border-border',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
),
},
}
function timeAgo(date) {
const seconds = Math.floor((new Date() - new Date(date)) / 1000)
if (seconds < 60) return 'just now'
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
return `${Math.floor(seconds / 86400)}d ago`
}
export default function Notifications() {
const [notifications, setNotifications] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Build notifications from tasks
fetch('/api/tasks', { headers: { Authorization: `Bearer ${getToken()}` } })
.then(r => r.json())
.then(tasks => {
if (!Array.isArray(tasks)) { setNotifications([]); return }
const notifs = []
// System welcome notification
notifs.push({
id: 'system-welcome',
type: 'system',
title: 'Welcome to TaskFlow',
message: 'Your workspace is ready. Start by creating your first task or inviting a team member.',
date: new Date().toISOString(),
read: false,
})
// Completed task notifications
tasks.filter(t => t.completed).forEach(task => {
notifs.push({
id: `completed-${task.id}`,
type: 'completed',
title: 'Task completed',
message: `"${task.title}" was marked as complete.`,
date: task.created_at,
read: false,
})
})
// Active task reminders
tasks.filter(t => !t.completed).slice(0, 2).forEach(task => {
notifs.push({
id: `reminder-${task.id}`,
type: 'deadline',
title: 'Task pending',
message: `"${task.title}" is still in progress. Stay on track!`,
date: task.created_at,
read: true,
})
})
setNotifications(notifs.sort((a, b) => new Date(b.date) - new Date(a.date)))
})
.catch(() => setNotifications([]))
.finally(() => setLoading(false))
}, [])
const markAllRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })))
}
const markRead = id => {
setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: true } : n))
}
const unreadCount = notifications.filter(n => !n.read).length
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-2xl mx-auto px-8 py-10">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight flex items-center gap-3">
Notifications
{unreadCount > 0 && (
<span className="bg-accent text-white text-xs font-700 px-2 py-0.5 rounded-full">
{unreadCount}
</span>
)}
</h1>
<p className="text-sm text-text2 mt-1">Stay updated on your workspace activity.</p>
</div>
{unreadCount > 0 && (
<button
onClick={markAllRead}
className="text-xs font-600 text-accent hover:underline transition-colors"
>
Mark all as read
</button>
)}
</div>
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{!loading && notifications.length === 0 && <EmptyNotifications />}
{!loading && notifications.length > 0 && (
<div className="flex flex-col gap-2">
{notifications.map((notif, i) => {
const config = TYPE_CONFIG[notif.type] || TYPE_CONFIG.system
return (
<div
key={notif.id}
onClick={() => markRead(notif.id)}
className={`flex items-start gap-4 p-4 rounded-xl border transition-all duration-150 cursor-pointer animate-fade-up
${notif.read ? 'bg-surface border-border opacity-70 hover:opacity-100' : 'bg-surface border-border hover:bg-surface2'}
`}
style={{ animationDelay: `${i * 0.04}s` }}
>
<div className={`w-8 h-8 rounded-full border flex items-center justify-center flex-shrink-0 ${config.bg} ${config.color}`}>
{config.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<p className="text-sm font-600 text-text1">{notif.title}</p>
{!notif.read && <span className="w-1.5 h-1.5 rounded-full bg-accent flex-shrink-0" />}
</div>
<p className="text-xs text-text2 leading-relaxed">{notif.message}</p>
</div>
<span className="text-[0.65rem] text-text3 flex-shrink-0">{timeAgo(notif.date)}</span>
</div>
)
})}
</div>
)}
</div>
</main>
</div>
)
}
+289
View File
@@ -0,0 +1,289 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import Footer from '../components/Footer'
const PLANS = [
{
name: 'Free',
monthly: 0,
yearly: 0,
description: 'Perfect for individuals and small personal projects.',
cta: 'Get Started Free',
ctaTo: '/register',
highlight: false,
features: [
'Up to 50 tasks',
'1 workspace',
'Basic task management',
'Email notifications',
'Mobile access',
'Community support',
],
missing: [
'Team collaboration',
'Analytics & reporting',
'Priority support',
'Custom integrations',
],
},
{
name: 'Pro',
monthly: 12,
yearly: 9,
description: 'For professionals and growing teams who need more power.',
cta: 'Start Pro Trial',
ctaTo: '/register',
highlight: true,
badge: 'Most Popular',
features: [
'Unlimited tasks',
'Up to 5 workspaces',
'Team collaboration',
'Advanced analytics',
'Priority notifications',
'File attachments',
'Activity feed',
'Priority email support',
],
missing: [
'Custom integrations',
'SSO authentication',
],
},
{
name: 'Enterprise',
monthly: 49,
yearly: 39,
description: 'For large teams and organizations that need full control.',
cta: 'Contact Sales',
ctaTo: '/contact',
highlight: false,
features: [
'Unlimited everything',
'Unlimited workspaces',
'SSO authentication',
'Custom integrations',
'Advanced security',
'Role-based permissions',
'SLA guarantee',
'Dedicated support',
'Custom onboarding',
'Audit logs',
],
missing: [],
},
]
const FAQS = [
{
q: 'Can I switch plans at any time?',
a: 'Yes. You can upgrade or downgrade your plan at any time. Changes take effect immediately and billing is prorated.',
},
{
q: 'Is there a free trial for paid plans?',
a: 'Yes. Pro and Enterprise plans come with a 14-day free trial. No credit card required to start.',
},
{
q: 'What happens to my data if I downgrade?',
a: 'Your data is safe. If you exceed the limits of a lower plan, you will be prompted to upgrade or archive excess data.',
},
{
q: 'Do you offer discounts for nonprofits or students?',
a: 'Yes. We offer 50% off Pro plans for verified nonprofits and students. Contact us to apply.',
},
{
q: 'How does team billing work?',
a: 'Plans are per workspace. You pay for one plan per workspace regardless of how many members are in it.',
},
]
function CheckIcon({ positive = true }) {
if (positive) return (
<div className="w-4 h-4 rounded-full bg-green-dim border border-green/30 flex items-center justify-center flex-shrink-0">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>
</div>
)
return (
<div className="w-4 h-4 rounded-full bg-surface2 border border-border flex items-center justify-center flex-shrink-0">
<svg width="7" height="7" viewBox="0 0 12 12" fill="none" stroke="#3d4257" strokeWidth="2.5"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg>
</div>
)
}
export default function Pricing() {
const [yearly, setYearly] = useState(false)
const [openFaq, setOpenFaq] = useState(null)
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</Link>
<nav className="hidden md:flex items-center gap-6">
<a href="/#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</a>
<Link to="/pricing" className="text-sm text-accent font-600 transition-colors">Pricing</Link>
<Link to="/about" className="text-sm text-text2 hover:text-text1 transition-colors">About</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className="py-20 border-b border-border text-center">
<div className="max-w-3xl mx-auto px-6">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-3">Pricing</p>
<h1 className="font-display font-800 text-4xl md:text-5xl text-text1 tracking-tight mb-5">
Simple, Transparent Pricing
</h1>
<p className="text-base text-text2 leading-relaxed mb-10">
Choose a plan that scales with your workflow -- whether you are managing personal tasks, startup operations, or enterprise-scale collaboration.
</p>
{/* Toggle */}
<div className="inline-flex items-center gap-3 bg-surface2 border border-border rounded-xl p-1.5">
<button
onClick={() => setYearly(false)}
className={`px-5 py-2 rounded-lg text-sm font-600 transition-all ${!yearly ? 'bg-accent text-white shadow-accent' : 'text-text2 hover:text-text1'}`}
>
Monthly
</button>
<button
onClick={() => setYearly(true)}
className={`px-5 py-2 rounded-lg text-sm font-600 transition-all flex items-center gap-2 ${yearly ? 'bg-accent text-white shadow-accent' : 'text-text2 hover:text-text1'}`}
>
Yearly
<span className={`text-[0.6rem] font-700 px-1.5 py-0.5 rounded-full ${yearly ? 'bg-white/20 text-white' : 'bg-green-dim text-green border border-green/20'}`}>
Save 25%
</span>
</button>
</div>
</div>
</section>
{/* Pricing cards */}
<section className="py-16 border-b border-border">
<div className="max-w-5xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{PLANS.map((plan, i) => (
<div
key={plan.name}
className={`rounded-2xl border p-6 flex flex-col transition-all duration-200 animate-fade-up
${plan.highlight
? 'bg-accent-dim border-accent/30 shadow-xl shadow-accent/10 scale-[1.02]'
: 'bg-surface border-border hover:border-text3'
}`}
style={{ animationDelay: `${i * 0.1}s` }}
>
{plan.badge && (
<div className="inline-flex items-center gap-1.5 bg-accent text-white text-[0.65rem] font-700 px-2.5 py-1 rounded-full mb-4 w-fit">
<span className="w-1 h-1 rounded-full bg-white/60 animate-pulse" />
{plan.badge}
</div>
)}
<h2 className="font-display font-800 text-text1 text-xl mb-1">{plan.name}</h2>
<p className="text-xs text-text2 mb-5 leading-relaxed">{plan.description}</p>
<div className="mb-6">
<span className="font-display font-800 text-4xl text-text1">
${yearly ? plan.yearly : plan.monthly}
</span>
<span className="text-text3 text-sm ml-1">/month</span>
{yearly && plan.monthly > 0 && (
<p className="text-xs text-green mt-1">Billed annually -- saves ${(plan.monthly - plan.yearly) * 12}/yr</p>
)}
</div>
<Link
to={plan.ctaTo}
className={`w-full py-3 rounded-xl text-sm font-700 text-center transition-all duration-150 mb-6
${plan.highlight
? 'bg-accent hover:bg-accent-hover text-white shadow-accent hover:-translate-y-0.5'
: 'bg-surface2 hover:bg-border text-text1 border border-border hover:border-text3'
}`}
>
{plan.cta}
</Link>
<div className="flex flex-col gap-2.5">
{plan.features.map(f => (
<div key={f} className="flex items-center gap-2.5">
<CheckIcon positive={true} />
<span className="text-xs text-text2">{f}</span>
</div>
))}
{plan.missing.map(f => (
<div key={f} className="flex items-center gap-2.5">
<CheckIcon positive={false} />
<span className="text-xs text-text3 line-through">{f}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</section>
{/* FAQ */}
<section className="py-16 border-b border-border">
<div className="max-w-2xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Frequently Asked Questions</h2>
<p className="text-sm text-text2">Everything you need to know about our pricing and plans.</p>
</div>
<div className="flex flex-col gap-2">
{FAQS.map((faq, i) => (
<div key={i} className="bg-surface border border-border rounded-xl overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full px-5 py-4 flex items-center justify-between text-left hover:bg-surface2 transition-colors"
>
<span className="text-sm font-600 text-text1">{faq.q}</span>
<svg
width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
className={`flex-shrink-0 text-text3 transition-transform duration-200 ${openFaq === i ? 'rotate-180' : ''}`}
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-sm text-text2 leading-relaxed border-t border-border pt-3">
{faq.a}
</div>
)}
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Start for free today</h2>
<p className="text-sm text-text2 mb-8">No credit card required. Upgrade when you are ready.</p>
<Link to="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Free
</Link>
</div>
</section>
<Footer />
</div>
)
}
+179
View File
@@ -0,0 +1,179 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import { EmptyProjects } from '../components/EmptyState'
const API = '/api'
function getToken() { return localStorage.getItem('token') }
function headers() {
return { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }
}
const COLORS = [
'bg-accent border-accent/30',
'bg-green border-green/30',
'bg-blue-500 border-blue-500/30',
'bg-purple-500 border-purple-500/30',
'bg-yellow-500 border-yellow-500/30',
'bg-red border-red/30',
]
export default function Projects() {
const [projects, setProjects] = useState([])
const [loading, setLoading] = useState(true)
const [showing, setShowing] = useState(false)
const [form, setForm] = useState({ name: '', description: '' })
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const fetchProjects = async () => {
try {
const res = await fetch(`${API}/projects`, { headers: { Authorization: `Bearer ${getToken()}` } })
const data = await res.json()
setProjects(Array.isArray(data) ? data : [])
} catch {
setProjects([])
} finally {
setLoading(false)
}
}
useEffect(() => { fetchProjects() }, [])
const handleCreate = async e => {
e.preventDefault()
if (!form.name.trim()) return
setSaving(true)
setError('')
try {
const res = await fetch(`${API}/projects`, { method: 'POST', headers: headers(), body: JSON.stringify(form) })
const data = await res.json()
if (!res.ok) { setError(data.error || 'Failed to create project'); return }
setForm({ name: '', description: '' })
setShowing(false)
fetchProjects()
} catch {
setError('Network error. Please try again.')
} finally {
setSaving(false)
}
}
const handleDelete = async id => {
if (!window.confirm('Delete this project?')) return
await fetch(`${API}/projects/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${getToken()}` } })
fetchProjects()
}
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-4xl mx-auto px-8 py-10">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Projects</h1>
<p className="text-sm text-text2 mt-1">Organize your work into focused project spaces.</p>
</div>
<button
onClick={() => setShowing(true)}
className="px-4 py-2.5 bg-accent hover:bg-accent-hover text-white text-sm font-700 rounded-lg transition-all duration-150 shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5 flex items-center gap-2"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New Project
</button>
</div>
{/* Create modal */}
{showing && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-surface border border-border rounded-2xl w-full max-w-md p-6 animate-fade-up">
<h2 className="font-display font-700 text-text1 text-lg mb-5">Create New Project</h2>
{error && (
<div className="mb-4 px-3 py-2.5 rounded-lg bg-red-dim border border-red/30 text-red text-sm">{error}</div>
)}
<form onSubmit={handleCreate} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Project Name</label>
<input
value={form.name}
onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
placeholder="e.g. AWS Infrastructure Setup"
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Description</label>
<textarea
value={form.description}
onChange={e => setForm(p => ({ ...p, description: e.target.value }))}
placeholder="What is this project about?"
rows={3}
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all resize-none"
/>
</div>
<div className="flex gap-3 mt-2">
<button type="button" onClick={() => { setShowing(false); setError('') }}
className="flex-1 py-2.5 bg-surface2 hover:bg-border text-text2 text-sm font-600 rounded-lg border border-border transition-all">
Cancel
</button>
<button type="submit" disabled={saving}
className="flex-1 py-2.5 bg-accent hover:bg-accent-hover disabled:opacity-60 text-white text-sm font-700 rounded-lg transition-all shadow-accent">
{saving ? 'Creating...' : 'Create Project'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Projects grid */}
{loading && (
<div className="flex items-center justify-center py-20">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{!loading && projects.length === 0 && (
<EmptyProjects onAdd={() => setShowing(true)} />
)}
{!loading && projects.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{projects.map((project, i) => (
<div key={project.id} className="bg-surface border border-border rounded-xl p-5 hover:border-accent/40 hover:bg-surface2 transition-all duration-150 group animate-fade-up">
<div className="flex items-start justify-between mb-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-white font-700 text-sm flex-shrink-0 ${COLORS[i % COLORS.length].split(' ')[0]}`}>
{project.name.charAt(0).toUpperCase()}
</div>
<button
onClick={() => handleDelete(project.id)}
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-lg text-text3 hover:text-red hover:bg-red-dim transition-all"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
</button>
</div>
<h3 className="font-display font-700 text-text1 text-sm mb-1">{project.name}</h3>
<p className="text-xs text-text2 leading-relaxed mb-4">{project.description || 'No description provided.'}</p>
<div className="flex items-center justify-between">
<span className="text-[0.65rem] text-text3">
{new Date(project.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
<span className="text-[0.65rem] font-600 text-accent bg-accent-dim border border-accent/20 px-2 py-0.5 rounded-full">
Active
</span>
</div>
</div>
))}
</div>
)}
</div>
</main>
</div>
)
}
+191
View File
@@ -0,0 +1,191 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
const API = '/api'
export default function Register() {
const navigate = useNavigate()
const [form, setForm] = useState({ name: '', email: '', password: '', confirm: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
const handleSubmit = async e => {
e.preventDefault()
setError('')
if (form.password !== form.confirm) { setError('Passwords do not match.'); return }
if (form.password.length < 8) { setError('Password must be at least 8 characters.'); return }
setLoading(true)
try {
const res = await fetch(`${API}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: form.name, email: form.email, password: form.password }),
})
const data = await res.json()
if (!res.ok) { setError(data.error || 'Registration failed'); return }
localStorage.setItem('token', data.token)
navigate('/dashboard')
} catch {
setError('Network error. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-bg flex">
{/* ---- Left branding panel ---- */}
<div className="hidden lg:flex lg:w-1/2 relative flex-col justify-between p-12 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-green/10 via-bg to-bg" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(34,197,94,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(34,197,94,0.04)_1px,transparent_1px)] bg-[size:40px_40px]" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-green/8 rounded-full blur-[100px] -translate-x-1/2 translate-y-1/2" />
<div className="relative z-10">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 text-lg tracking-tight">TaskFlow</span>
</Link>
</div>
<div className="relative z-10">
<h2 className="font-display font-800 text-3xl text-text1 leading-tight tracking-tight mb-4">
Start organizing tasks, collaborating with your team, and scaling productivity from one platform.
</h2>
<p className="text-text2 text-sm leading-relaxed max-w-sm">
Free to start. Production-grade infrastructure. Built to grow with your team.
</p>
<div className="mt-10 flex flex-col gap-3">
{[
'Unlimited tasks on free tier',
'Team workspaces included',
'Secure by default -- JWT + PostgreSQL',
].map(item => (
<div key={item} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-accent-dim border border-accent/30 flex items-center justify-center flex-shrink-0">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#6366f1" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>
</div>
<span className="text-sm text-text2">{item}</span>
</div>
))}
</div>
</div>
<div className="relative z-10">
<p className="text-xs text-text3">
Built by{' '}
<a href="https://www.linkedin.com/in/ikechukwu-elizabeth" target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">
Elizabeth Ikechukwu
</a>
{' '}- DevOps &amp; Cloud Engineer
</p>
</div>
</div>
{/* ---- Right auth panel ---- */}
<div className="flex-1 flex items-center justify-center px-6 py-16">
<div className="w-full max-w-sm">
<div className="flex items-center gap-2.5 mb-10 lg:hidden">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1">TaskFlow</span>
</div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight mb-1">Create your account</h1>
<p className="text-sm text-text2 mb-8">Get started free. No credit card required.</p>
{error && (
<div className="mb-5 px-4 py-3 rounded-lg bg-red-dim border border-red/30 text-red text-sm flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Full name</label>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
placeholder="Elizabeth Ikechukwu"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Email address</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="you@example.com"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Password</label>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="Min. 8 characters"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Confirm password</label>
<input
type="password"
name="confirm"
value={form.confirm}
onChange={handleChange}
placeholder="Repeat password"
required
className="w-full bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all duration-150"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full mt-2 py-3 bg-accent hover:bg-accent-hover disabled:opacity-60 disabled:cursor-not-allowed text-white font-700 rounded-lg text-sm transition-all duration-150 shadow-accent hover:shadow-accent-lg hover:-translate-y-0.5 active:translate-y-0"
>
{loading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<p className="mt-6 text-center text-xs text-text3">
By creating an account you agree to our{' '}
<a href="#" className="text-text2 hover:text-accent transition-colors">Terms of Service</a>
{' '}and{' '}
<a href="#" className="text-text2 hover:text-accent transition-colors">Privacy Policy</a>.
</p>
<p className="mt-5 text-center text-sm text-text2">
Already have an account?{' '}
<Link to="/login" className="text-accent hover:underline font-600">Sign in</Link>
</p>
</div>
</div>
</div>
)
}
+208
View File
@@ -0,0 +1,208 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
const API = '/api'
function getToken() { return localStorage.getItem('token') }
function getUserFromToken() {
try {
const token = localStorage.getItem('token')
if (!token) return null
return JSON.parse(atob(token.split('.')[1]))
} catch { return null }
}
function Section({ title, description, children }) {
return (
<div className="bg-surface border border-border rounded-xl overflow-hidden mb-4">
<div className="px-6 py-4 border-b border-border">
<h2 className="font-display font-700 text-text1 text-sm">{title}</h2>
{description && <p className="text-xs text-text3 mt-0.5">{description}</p>}
</div>
<div className="px-6 py-5">{children}</div>
</div>
)
}
export default function Settings() {
const tokenUser = getUserFromToken()
const [profile, setProfile] = useState({ name: tokenUser?.name || '', email: tokenUser?.email || '' })
const [password, setPassword] = useState({ current: '', next: '', confirm: '' })
const [profileMsg, setProfileMsg] = useState({ text: '', ok: true })
const [passwordMsg, setPasswordMsg] = useState({ text: '', ok: true })
const [savingProfile, setSavingProfile] = useState(false)
const [savingPassword, setSavingPassword] = useState(false)
const handleProfileSave = async e => {
e.preventDefault()
setSavingProfile(true)
setProfileMsg({ text: '', ok: true })
try {
const res = await fetch(`${API}/auth/me`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ name: profile.name, email: profile.email }),
})
const data = await res.json()
if (!res.ok) { setProfileMsg({ text: data.error || 'Update failed', ok: false }); return }
setProfileMsg({ text: 'Profile updated successfully.', ok: true })
} catch {
setProfileMsg({ text: 'Network error. Please try again.', ok: false })
} finally {
setSavingProfile(false)
}
}
const handlePasswordSave = async e => {
e.preventDefault()
setPasswordMsg({ text: '', ok: true })
if (password.next !== password.confirm) {
setPasswordMsg({ text: 'New passwords do not match.', ok: false }); return
}
if (password.next.length < 8) {
setPasswordMsg({ text: 'Password must be at least 8 characters.', ok: false }); return
}
setSavingPassword(true)
try {
const res = await fetch(`${API}/auth/password`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ currentPassword: password.current, newPassword: password.next }),
})
const data = await res.json()
if (!res.ok) { setPasswordMsg({ text: data.error || 'Password update failed', ok: false }); return }
setPasswordMsg({ text: 'Password updated successfully.', ok: true })
setPassword({ current: '', next: '', confirm: '' })
} catch {
setPasswordMsg({ text: 'Network error. Please try again.', ok: false })
} finally {
setSavingPassword(false)
}
}
const initials = profile.name
? profile.name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2)
: 'U'
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-2xl mx-auto px-8 py-10">
<div className="mb-8">
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Settings</h1>
<p className="text-sm text-text2 mt-1">Manage your account and workspace preferences.</p>
</div>
{/* Avatar section */}
<Section title="Profile Picture" description="Your avatar is generated from your name initials.">
<div className="flex items-center gap-5">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-accent to-green flex items-center justify-center text-white text-xl font-700 flex-shrink-0">
{initials}
</div>
<div>
<p className="text-sm font-600 text-text1">{profile.name || 'Your Name'}</p>
<p className="text-xs text-text3 mt-0.5">{profile.email || 'your@email.com'}</p>
<p className="text-xs text-text3 mt-2">Avatar is auto-generated from your name. Custom avatars coming soon.</p>
</div>
</div>
</Section>
{/* Profile section */}
<Section title="Profile Information" description="Update your name and email address.">
{profileMsg.text && (
<div className={`mb-4 px-3 py-2.5 rounded-lg text-sm border ${profileMsg.ok ? 'bg-green-dim border-green/30 text-green' : 'bg-red-dim border-red/30 text-red'}`}>
{profileMsg.text}
</div>
)}
<form onSubmit={handleProfileSave} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Full Name</label>
<input
value={profile.name}
onChange={e => setProfile(p => ({ ...p, name: e.target.value }))}
placeholder="Your full name"
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Email Address</label>
<input
type="email"
value={profile.email}
onChange={e => setProfile(p => ({ ...p, email: e.target.value }))}
placeholder="you@example.com"
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
<div>
<button
type="submit"
disabled={savingProfile}
className="px-5 py-2.5 bg-accent hover:bg-accent-hover disabled:opacity-60 text-white text-sm font-700 rounded-lg transition-all shadow-accent"
>
{savingProfile ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
</Section>
{/* Password section */}
<Section title="Change Password" description="Update your password. Minimum 8 characters.">
{passwordMsg.text && (
<div className={`mb-4 px-3 py-2.5 rounded-lg text-sm border ${passwordMsg.ok ? 'bg-green-dim border-green/30 text-green' : 'bg-red-dim border-red/30 text-red'}`}>
{passwordMsg.text}
</div>
)}
<form onSubmit={handlePasswordSave} className="flex flex-col gap-4">
{[
{ label: 'Current Password', key: 'current', placeholder: 'Enter current password' },
{ label: 'New Password', key: 'next', placeholder: 'Min. 8 characters' },
{ label: 'Confirm New Password', key: 'confirm', placeholder: 'Repeat new password' },
].map(f => (
<div key={f.key}>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">{f.label}</label>
<input
type="password"
value={password[f.key]}
onChange={e => setPassword(p => ({ ...p, [f.key]: e.target.value }))}
placeholder={f.placeholder}
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
))}
<div>
<button
type="submit"
disabled={savingPassword}
className="px-5 py-2.5 bg-accent hover:bg-accent-hover disabled:opacity-60 text-white text-sm font-700 rounded-lg transition-all shadow-accent"
>
{savingPassword ? 'Updating...' : 'Update Password'}
</button>
</div>
</form>
</Section>
{/* Danger zone */}
<Section title="Danger Zone" description="Irreversible actions. Proceed with caution.">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-600 text-text1">Delete Account</p>
<p className="text-xs text-text3 mt-0.5">Permanently delete your account and all associated data.</p>
</div>
<button
onClick={() => alert('Account deletion requires contacting support. This prevents accidental data loss.')}
className="px-4 py-2 bg-red-dim border border-red/30 text-red text-xs font-700 rounded-lg hover:bg-red hover:text-white transition-all"
>
Delete Account
</button>
</div>
</Section>
</div>
</main>
</div>
)
}
+196
View File
@@ -0,0 +1,196 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Footer from '../components/Footer'
const SERVICES = [
{ name: 'API', description: 'REST API endpoints', endpoint: '/api/health' },
{ name: 'Frontend', description: 'Web application', endpoint: null },
{ name: 'Database', description: 'PostgreSQL data layer', endpoint: null },
{ name: 'Authentication', description: 'JWT auth service', endpoint: null },
{ name: 'Notifications', description: 'Alert delivery system', endpoint: null },
{ name: 'File Storage', description: 'Asset and attachment storage', endpoint: null },
]
const INCIDENTS = [
{
date: 'May 28, 2026',
title: 'All systems operational',
status: 'resolved',
message: 'No incidents reported. Platform running normally.',
},
{
date: 'May 20, 2026',
title: 'Scheduled maintenance -- database upgrade',
status: 'resolved',
message: 'PostgreSQL version upgrade completed successfully. Downtime: 4 minutes.',
},
{
date: 'May 14, 2026',
title: 'API latency increase',
status: 'resolved',
message: 'Briefly elevated response times due to high traffic. Auto-scaling resolved the issue within 8 minutes.',
},
]
const UPTIME = [
{ service: 'API', uptime: '99.98%', days: 30 },
{ service: 'Frontend', uptime: '100%', days: 30 },
{ service: 'Database', uptime: '99.95%', days: 30 },
{ service: 'Auth Service', uptime: '99.99%', days: 30 },
]
function StatusBadge({ status }) {
if (status === 'operational') return (
<div className="flex items-center gap-1.5 text-xs font-600 text-green">
<span className="w-2 h-2 rounded-full bg-green animate-pulse" />
Operational
</div>
)
if (status === 'degraded') return (
<div className="flex items-center gap-1.5 text-xs font-600 text-yellow-400">
<span className="w-2 h-2 rounded-full bg-yellow-400" />
Degraded
</div>
)
if (status === 'down') return (
<div className="flex items-center gap-1.5 text-xs font-600 text-red">
<span className="w-2 h-2 rounded-full bg-red" />
Down
</div>
)
return null
}
export default function Status() {
const [apiStatus, setApiStatus] = useState('checking')
const [apiLatency, setApiLatency] = useState(null)
useEffect(() => {
const start = Date.now()
fetch('/api/health')
.then(r => r.json())
.then(() => {
setApiStatus('operational')
setApiLatency(Date.now() - start)
})
.catch(() => setApiStatus('down'))
}, [])
const getServiceStatus = (name) => {
if (name === 'API') return apiStatus === 'checking' ? 'operational' : apiStatus
return 'operational'
}
const allOperational = SERVICES.every(s => getServiceStatus(s.name) === 'operational')
return (
<div className="min-h-screen bg-bg flex flex-col">
{/* Navbar */}
<header className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-accent flex items-center justify-center glow-pulse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<span className="font-display font-800 text-text1 tracking-tight">TaskFlow</span>
</Link>
<nav className="hidden md:flex items-center gap-6">
<a href="/#features" className="text-sm text-text2 hover:text-text1 transition-colors">Features</a>
<Link to="/pricing" className="text-sm text-text2 hover:text-text1 transition-colors">Pricing</Link>
<Link to="/about" className="text-sm text-text2 hover:text-text1 transition-colors">About</Link>
</nav>
<div className="flex items-center gap-3">
<Link to="/login" className="text-sm text-text2 hover:text-text1 transition-colors px-3 py-1.5">Sign In</Link>
<Link to="/register" className="text-sm font-600 bg-accent hover:bg-accent-hover text-white px-4 py-2 rounded-lg transition-all shadow-accent hover:-translate-y-0.5">
Get Started
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className={`py-16 border-b border-border text-center ${allOperational ? 'bg-green-dim/30' : 'bg-red-dim/30'}`}>
<div className="max-w-2xl mx-auto px-6">
<div className={`inline-flex items-center gap-2.5 px-5 py-2.5 rounded-full border mb-5 ${allOperational ? 'bg-green-dim border-green/30' : 'bg-red-dim border-red/30'}`}>
<span className={`w-2.5 h-2.5 rounded-full animate-pulse ${allOperational ? 'bg-green' : 'bg-red'}`} />
<span className={`text-sm font-700 ${allOperational ? 'text-green' : 'text-red'}`}>
{allOperational ? 'All Systems Operational' : 'Service Disruption Detected'}
</span>
</div>
<h1 className="font-display font-800 text-3xl text-text1 tracking-tight mb-3">System Status</h1>
<p className="text-sm text-text2">
Monitor platform uptime, API availability, infrastructure health, and operational performance in real time.
</p>
<p className="text-xs text-text3 mt-3">
Last checked: {new Date().toLocaleString()}
</p>
</div>
</section>
{/* Services */}
<section className="py-12 border-b border-border">
<div className="max-w-3xl mx-auto px-6">
<h2 className="font-display font-700 text-text1 text-sm uppercase tracking-wider mb-4">Service Health</h2>
<div className="bg-surface border border-border rounded-2xl overflow-hidden">
{SERVICES.map((service, i) => (
<div key={service.name} className={`flex items-center justify-between px-5 py-4 ${i < SERVICES.length - 1 ? 'border-b border-border' : ''} hover:bg-surface2 transition-colors`}>
<div>
<p className="text-sm font-600 text-text1">{service.name}</p>
<p className="text-xs text-text3 mt-0.5">{service.description}</p>
</div>
<div className="flex items-center gap-4">
{service.name === 'API' && apiLatency && (
<span className="text-xs text-text3">{apiLatency}ms</span>
)}
<StatusBadge status={getServiceStatus(service.name)} />
</div>
</div>
))}
</div>
</div>
</section>
{/* Uptime */}
<section className="py-12 border-b border-border">
<div className="max-w-3xl mx-auto px-6">
<h2 className="font-display font-700 text-text1 text-sm uppercase tracking-wider mb-4">30-Day Uptime</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{UPTIME.map((u, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-4 text-center">
<p className="font-display font-800 text-xl text-green mb-1">{u.uptime}</p>
<p className="text-xs text-text2">{u.service}</p>
<p className="text-[0.6rem] text-text3 mt-1">Last {u.days} days</p>
</div>
))}
</div>
</div>
</section>
{/* Incident history */}
<section className="py-12">
<div className="max-w-3xl mx-auto px-6">
<h2 className="font-display font-700 text-text1 text-sm uppercase tracking-wider mb-4">Incident History</h2>
<div className="flex flex-col gap-3">
{INCIDENTS.map((incident, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5">
<div className="flex items-start justify-between gap-4 mb-2">
<h3 className="text-sm font-600 text-text1">{incident.title}</h3>
<span className={`text-[0.65rem] font-600 px-2 py-0.5 rounded-full border flex-shrink-0 ${incident.status === 'resolved' ? 'text-green bg-green-dim border-green/20' : 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20'}`}>
{incident.status}
</span>
</div>
<p className="text-xs text-text2 leading-relaxed mb-2">{incident.message}</p>
<p className="text-[0.65rem] text-text3">{incident.date}</p>
</div>
))}
</div>
</div>
</section>
<Footer />
</div>
)
}
+180
View File
@@ -0,0 +1,180 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import { EmptyTasks } from '../components/EmptyState'
const API = '/api'
function getToken() { return localStorage.getItem('token') }
function authHeader() { return { Authorization: `Bearer ${getToken()}` } }
export default function Tasks() {
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
const [title, setTitle] = useState('')
const [adding, setAdding] = useState(false)
const [filter, setFilter] = useState('all') // all | active | completed
const fetchTasks = async () => {
try {
const res = await fetch(`${API}/tasks`, { headers: authHeader() })
const data = await res.json()
setTasks(Array.isArray(data) ? data : [])
} catch { setTasks([]) }
finally { setLoading(false) }
}
useEffect(() => { fetchTasks() }, [])
const addTask = async () => {
if (!title.trim()) return
setAdding(true)
await fetch(`${API}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeader() },
body: JSON.stringify({ title }),
})
setTitle('')
setAdding(false)
fetchTasks()
}
const toggleTask = async id => {
await fetch(`${API}/tasks/${id}`, { method: 'PATCH', headers: authHeader() })
fetchTasks()
}
const deleteTask = async id => {
await fetch(`${API}/tasks/${id}`, { method: 'DELETE', headers: authHeader() })
fetchTasks()
}
const filtered = tasks.filter(t =>
filter === 'all' ? true : filter === 'active' ? !t.completed : t.completed
)
const completed = tasks.filter(t => t.completed).length
const active = tasks.filter(t => !t.completed).length
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-3xl mx-auto px-8 py-10">
{/* Header */}
<div className="mb-8">
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Tasks</h1>
<p className="text-sm text-text2 mt-1">Manage and track all your tasks in one place.</p>
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-4 mb-6">
{[
{ label: 'Total', value: tasks.length, color: 'text-text1' },
{ label: 'Active', value: active, color: 'text-[#818cf8]' },
{ label: 'Completed', value: completed, color: 'text-green' },
].map(s => (
<div key={s.label} className="bg-surface border border-border rounded-xl p-4 text-center">
<p className={`font-display font-800 text-2xl ${s.color}`}>{s.value}</p>
<p className="text-xs text-text2 uppercase tracking-wider mt-1">{s.label}</p>
</div>
))}
</div>
{/* Input */}
<div className="flex gap-3 mb-6">
<input
value={title}
onChange={e => setTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTask()}
placeholder="Add a new task and press Enter..."
className="flex-1 bg-surface border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
<button
onClick={addTask}
disabled={adding || !title.trim()}
className="px-5 py-3 bg-accent hover:bg-accent-hover disabled:opacity-50 text-white text-sm font-700 rounded-lg transition-all shadow-accent whitespace-nowrap"
>
{adding ? '...' : '+ Add'}
</button>
</div>
{/* Filter tabs */}
<div className="flex gap-1 mb-6 bg-surface2 border border-border rounded-lg p-1 w-fit">
{['all', 'active', 'completed'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-4 py-1.5 rounded-md text-xs font-600 capitalize transition-all duration-150
${filter === f ? 'bg-accent text-white shadow-accent' : 'text-text2 hover:text-text1'}`}
>
{f}
</button>
))}
</div>
{/* Task list */}
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{!loading && tasks.length === 0 && (
<EmptyTasks onAdd={() => document.querySelector('input')?.focus()} />
)}
{!loading && tasks.length > 0 && filtered.length === 0 && (
<div className="text-center py-16">
<p className="text-text2 text-sm">No {filter} tasks found.</p>
</div>
)}
{!loading && filtered.length > 0 && (
<div className="flex flex-col gap-2">
{filtered.map(task => (
<div
key={task.id}
className={`flex items-center gap-3 rounded-xl border px-4 py-3.5 transition-all duration-150 animate-slide-in
${task.completed
? 'bg-green-dim border-green/20'
: 'bg-surface border-border hover:border-accent/40 hover:bg-surface2'
}`}
>
<button
onClick={() => toggleTask(task.id)}
className={`w-5 h-5 rounded-md border-2 flex items-center justify-center flex-shrink-0 transition-all
${task.completed ? 'bg-green border-green' : 'border-text3 hover:border-green hover:bg-green-dim'}`}
>
{task.completed && (
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2.5">
<polyline points="1.5,6 5,9.5 10.5,2.5"/>
</svg>
)}
</button>
<span className={`flex-1 text-sm font-500 ${task.completed ? 'line-through text-text3 decoration-green' : 'text-text1'}`}>
{task.title}
</span>
<span className="text-[0.65rem] text-text3 flex-shrink-0 hidden sm:block">
{new Date(task.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<button
onClick={() => deleteTask(task.id)}
className="flex-shrink-0 p-1.5 rounded-lg text-text3 hover:text-red hover:bg-red-dim transition-all"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14H6L5 6"/>
<path d="M10 11v6M14 11v6"/>
<path d="M9 6V4h6v2"/>
</svg>
</button>
</div>
))}
</div>
)}
</div>
</main>
</div>
)
}
+212
View File
@@ -0,0 +1,212 @@
import { useEffect, useState } from 'react'
import Sidebar from '../components/Sidebar'
import { EmptyTeam } from '../components/EmptyState'
const API = '/api'
function getToken() { return localStorage.getItem('token') }
function getUserFromToken() {
try {
const token = localStorage.getItem('token')
if (!token) return null
return JSON.parse(atob(token.split('.')[1]))
} catch { return null }
}
export default function Teams() {
const [members, setMembers] = useState([])
const [loading, setLoading] = useState(true)
const [showing, setShowing] = useState(false)
const [email, setEmail] = useState('')
const [role, setRole] = useState('member')
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const currentUser = getUserFromToken()
const fetchMembers = async () => {
try {
const res = await fetch(`${API}/team`, { headers: { Authorization: `Bearer ${getToken()}` } })
const data = await res.json()
setMembers(Array.isArray(data) ? data : [])
} catch { setMembers([]) }
finally { setLoading(false) }
}
useEffect(() => { fetchMembers() }, [])
const handleInvite = async e => {
e.preventDefault()
if (!email.trim()) return
setSaving(true)
setError('')
setSuccess('')
try {
const res = await fetch(`${API}/team/invite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ email, role }),
})
const data = await res.json()
if (!res.ok) { setError(data.error || 'Failed to send invite'); return }
setSuccess(`Invitation sent to ${email}`)
setEmail('')
setShowing(false)
fetchMembers()
} catch {
setError('Network error. Please try again.')
} finally {
setSaving(false)
}
}
const handleRemove = async id => {
if (!window.confirm('Remove this team member?')) return
await fetch(`${API}/team/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${getToken()}` },
})
fetchMembers()
}
const ROLE_COLORS = {
admin: 'text-accent bg-accent-dim border-accent/20',
member: 'text-green bg-green-dim border-green/20',
viewer: 'text-text2 bg-surface2 border-border',
}
return (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="ml-[240px] flex-1 min-h-screen overflow-y-auto">
<div className="max-w-3xl mx-auto px-8 py-10">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="font-display font-800 text-2xl text-text1 tracking-tight">Teams</h1>
<p className="text-sm text-text2 mt-1">Manage your workspace members and roles.</p>
</div>
<button
onClick={() => setShowing(true)}
className="px-4 py-2.5 bg-accent hover:bg-accent-hover text-white text-sm font-700 rounded-lg transition-all shadow-accent hover:-translate-y-0.5 flex items-center gap-2"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Invite Member
</button>
</div>
{success && (
<div className="mb-5 px-4 py-3 rounded-lg bg-green-dim border border-green/30 text-green text-sm flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="20 6 9 17 4 12"/></svg>
{success}
</div>
)}
{/* Invite modal */}
{showing && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-surface border border-border rounded-2xl w-full max-w-md p-6 animate-fade-up">
<h2 className="font-display font-700 text-text1 text-lg mb-5">Invite Team Member</h2>
{error && (
<div className="mb-4 px-3 py-2.5 rounded-lg bg-red-dim border border-red/30 text-red text-sm">{error}</div>
)}
<form onSubmit={handleInvite} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Email Address</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="teammate@example.com"
required
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 placeholder-text3 outline-none focus:border-accent focus:ring-2 focus:ring-accent/15 transition-all"
/>
</div>
<div>
<label className="block text-xs font-600 text-text2 uppercase tracking-wider mb-2">Role</label>
<select
value={role}
onChange={e => setRole(e.target.value)}
className="w-full bg-surface2 border border-border rounded-lg px-4 py-3 text-sm text-text1 outline-none focus:border-accent transition-all"
>
<option value="admin">Admin -- full access</option>
<option value="member">Member -- can create and edit</option>
<option value="viewer">Viewer -- read only</option>
</select>
</div>
<div className="flex gap-3 mt-2">
<button type="button" onClick={() => { setShowing(false); setError('') }}
className="flex-1 py-2.5 bg-surface2 hover:bg-border text-text2 text-sm font-600 rounded-lg border border-border transition-all">
Cancel
</button>
<button type="submit" disabled={saving}
className="flex-1 py-2.5 bg-accent hover:bg-accent-hover disabled:opacity-60 text-white text-sm font-700 rounded-lg transition-all shadow-accent">
{saving ? 'Sending...' : 'Send Invite'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Members list */}
<div className="bg-surface border border-border rounded-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
<h2 className="font-display font-700 text-text1 text-sm tracking-tight">Workspace Members</h2>
<span className="text-xs text-text3 bg-surface2 border border-border px-2.5 py-1 rounded-full">
{members.length + 1} member{members.length !== 0 ? 's' : ''}
</span>
</div>
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)}
{/* Current user always shown */}
<div className="px-6 py-4 flex items-center gap-4 border-b border-border bg-surface2">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-accent to-green flex items-center justify-center text-white text-xs font-700 flex-shrink-0">
{currentUser?.name?.charAt(0)?.toUpperCase() || 'Y'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-600 text-text1">{currentUser?.name || 'You'} <span className="text-text3 font-400">(you)</span></p>
<p className="text-xs text-text3">{currentUser?.email || ''}</p>
</div>
<span className="text-[0.65rem] font-600 px-2 py-0.5 rounded-full border text-accent bg-accent-dim border-accent/20">
Admin
</span>
</div>
{!loading && members.length === 0 && (
<EmptyTeam onInvite={() => setShowing(true)} />
)}
{!loading && members.map(member => (
<div key={member.id} className="px-6 py-4 flex items-center gap-4 border-b border-border last:border-0 hover:bg-surface2 transition-colors group">
<div className="w-9 h-9 rounded-full bg-surface2 border border-border flex items-center justify-center text-text2 text-xs font-700 flex-shrink-0">
{member.name?.charAt(0)?.toUpperCase() || member.email?.charAt(0)?.toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-600 text-text1 truncate">{member.name || 'Invited User'}</p>
<p className="text-xs text-text3 truncate">{member.email}</p>
</div>
<span className={`text-[0.65rem] font-600 px-2 py-0.5 rounded-full border ${ROLE_COLORS[member.role] || ROLE_COLORS.member}`}>
{member.role || 'member'}
</span>
<button
onClick={() => handleRemove(member.id)}
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-lg text-text3 hover:text-red hover:bg-red-dim transition-all"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
))}
</div>
</div>
</main>
</div>
)
}
@@ -0,0 +1,128 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Data-Driven Productivity Insights',
description: 'Monitor team performance, task completion rates, operational efficiency, and project progress through visual analytics and reporting tools. Turn raw activity into actionable insights.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
),
color: 'text-purple-400',
bg: 'bg-purple-400/10 border-purple-400/20',
}
const CAPABILITIES = [
{ title: 'Productivity Charts', description: 'Bar and line charts showing tasks created and completed over time. See your velocity and identify slowdowns before they become problems.', icon: '📈' },
{ title: 'Completion Metrics', description: 'Track your overall completion rate, average task age, and streak of productive days to build consistent habits.', icon: '✅' },
{ title: 'Performance Reports', description: 'Weekly and monthly summaries of team output. Shareable reports for standups, retrospectives, and stakeholder updates.', icon: '📋' },
{ title: 'Activity Monitoring', description: 'See which projects and team members are most active. Identify blockers and redistribute work before deadlines are missed.', icon: '👁️' },
{ title: 'Task Breakdown', description: 'Pie charts showing the ratio of active vs completed tasks, filtered by project, team member, or label.', icon: '🥧' },
{ title: 'Trend Analysis', description: 'Week-over-week and month-over-month trend lines showing whether your team is accelerating or slowing down.', icon: '📊' },
]
const MOCK_BARS = [
{ day: 'Mon', created: 4, completed: 2 },
{ day: 'Tue', created: 2, completed: 3 },
{ day: 'Wed', created: 6, completed: 5 },
{ day: 'Thu', created: 3, completed: 4 },
{ day: 'Fri', created: 5, completed: 6 },
{ day: 'Sat', created: 1, completed: 2 },
{ day: 'Sun', created: 2, completed: 1 },
]
const MAX_VAL = 8
export default function AnalyticsFeature() {
return (
<FeaturePageLayout hero={HERO}>
{/* Chart preview */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Live Preview</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">Your analytics dashboard</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 max-w-4xl mx-auto">
{/* Stats */}
<div className="flex flex-col gap-4">
{[
{ label: 'Total Tasks', value: '24', color: 'text-text1' },
{ label: 'Completed', value: '18', color: 'text-green' },
{ label: 'Completion Rate', value: '75%', color: 'text-accent' },
].map(s => (
<div key={s.label} className="bg-surface border border-border rounded-xl p-4 text-center">
<p className={`font-display font-800 text-3xl ${s.color}`}>{s.value}</p>
<p className="text-xs text-text2 uppercase tracking-wider mt-1">{s.label}</p>
</div>
))}
</div>
{/* Bar chart mock */}
<div className="lg:col-span-2 bg-surface border border-border rounded-xl p-5">
<h3 className="font-display font-700 text-text1 text-xs mb-4 uppercase tracking-wider">Daily Activity -- Last 7 Days</h3>
<div className="flex items-end gap-2 h-32">
{MOCK_BARS.map((b, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div className="w-full flex flex-col items-center gap-0.5">
<div
className="w-full bg-accent rounded-t transition-all"
style={{ height: `${(b.created / MAX_VAL) * 80}px` }}
/>
<div
className="w-full bg-green rounded-t transition-all"
style={{ height: `${(b.completed / MAX_VAL) * 80}px` }}
/>
</div>
<span className="text-[0.6rem] text-text3">{b.day}</span>
</div>
))}
</div>
<div className="flex items-center gap-4 mt-3">
<div className="flex items-center gap-1.5"><div className="w-2.5 h-2.5 rounded-sm bg-accent" /><span className="text-[0.65rem] text-text2">Created</span></div>
<div className="flex items-center gap-1.5"><div className="w-2.5 h-2.5 rounded-sm bg-green" /><span className="text-[0.65rem] text-text2">Completed</span></div>
</div>
</div>
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Measure what matters</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Real data from your actual work -- no vanity metrics, just the numbers that help you ship better.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Start tracking your productivity</h2>
<p className="text-sm text-text2 mb-8">See your real numbers and build the habit of consistent, measurable output.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
View Your Analytics
</a>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,143 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Built for Scale and Reliability',
description: 'Designed with scalable cloud infrastructure and modern engineering practices to support growing teams, increasing workloads, and enterprise-level operations. Built by a DevOps engineer, for engineers.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
),
color: 'text-accent',
bg: 'bg-accent-dim border-accent/20',
}
const STACK = [
{ layer: 'Frontend', tech: 'React + Vite + Tailwind CSS', note: 'Built as a static bundle, served by Nginx', color: 'text-blue-400', bg: 'bg-blue-400/10 border-blue-400/20' },
{ layer: 'Reverse Proxy', tech: 'Nginx', note: 'Forwards /api/* to Express, serves static files', color: 'text-green', bg: 'bg-green-dim border-green/20' },
{ layer: 'Backend API', tech: 'Node.js + Express', note: 'REST API with JWT auth, scoped to authenticated users', color: 'text-accent', bg: 'bg-accent-dim border-accent/20' },
{ layer: 'Database', tech: 'PostgreSQL on AWS RDS', note: 'Managed RDS instance in a private subnet, t3.micro', color: 'text-purple-400', bg: 'bg-purple-400/10 border-purple-400/20' },
{ layer: 'Compute', tech: 'AWS EC2 t3.micro', note: 'Docker Compose runs Nginx + Node in containers', color: 'text-yellow-400', bg: 'bg-yellow-400/10 border-yellow-400/20' },
{ layer: 'Infrastructure', tech: 'Terraform', note: 'VPC, EC2, RDS, IAM, S3 backend, security groups as code', color: 'text-red-400', bg: 'bg-red-400/10 border-red-400/20' },
{ layer: 'CI/CD', tech: 'GitHub Actions + OIDC', note: 'Terraform plan on PR, Terraform apply and deploy on merge', color: 'text-accent', bg: 'bg-accent-dim border-accent/20' },
{ layer: 'Secrets', tech: 'AWS SSM Parameter Store', note: 'DB credentials fetched at EC2 startup, never in code', color: 'text-green', bg: 'bg-green-dim border-green/20' },
]
const CAPABILITIES = [
{ title: 'Infrastructure as Code', description: 'Every AWS resource -- VPC, EC2, RDS, IAM, security groups -- is defined in Terraform. Reproducible, versionable, reviewable.', icon: '📝' },
{ title: 'Containerized Workloads', description: 'Frontend and backend run in Docker containers orchestrated with Docker Compose. Consistent environments from local to production.', icon: '🐳' },
{ title: 'Automated CI/CD', description: 'Every push to main triggers a GitHub Actions pipeline that builds, tests, and deploys automatically using OIDC auth.', icon: '🚀' },
{ title: 'Private Networking', description: 'RDS lives in a private subnet with no public access. Only the EC2 instance can reach the database through a security group rule.', icon: '🔒' },
{ title: 'High Availability', description: 'AWS RDS supports Multi-AZ for automatic failover. The architecture is designed to scale to EKS when the time comes.', icon: '🏗️' },
{ title: 'Kubernetes Ready', description: 'The entire stack is containerized and ready to migrate to EKS. Helm charts and Kubernetes manifests are the next step.', icon: '☸️' },
]
export default function Architecture() {
return (
<FeaturePageLayout hero={HERO}>
{/* Stack diagram */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Tech Stack</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">Layer by layer</h2>
</div>
<div className="max-w-3xl mx-auto flex flex-col gap-2">
{STACK.map((s, i) => (
<div key={i} className="flex items-start gap-4 bg-surface border border-border rounded-xl px-5 py-4 hover:bg-surface2 transition-colors group">
<div className={`px-2.5 py-1 rounded-lg border text-xs font-700 flex-shrink-0 w-32 text-center ${s.bg} ${s.color}`}>
{s.layer}
</div>
<div className="flex-1">
<p className="text-sm font-600 text-text1 mb-0.5">{s.tech}</p>
<p className="text-xs text-text3">{s.note}</p>
</div>
<div className="w-px h-8 bg-border self-center hidden sm:block" />
<div className="w-6 h-6 rounded-full bg-green-dim border border-green/30 flex items-center justify-center flex-shrink-0 hidden sm:flex">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>
</div>
</div>
))}
</div>
</div>
</section>
{/* DevOps pipeline */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">CI/CD Pipeline</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">From commit to production</h2>
</div>
<div className="flex flex-wrap items-center justify-center gap-2 max-w-4xl mx-auto">
{[
{ step: '1', label: 'Push to GitHub', icon: '📤' },
{ step: '2', label: 'GitHub Actions triggers', icon: '⚙️' },
{ step: '3', label: 'Build Docker images', icon: '🐳' },
{ step: '4', label: 'Push to ECR', icon: '📦' },
{ step: '5', label: 'Terraform apply', icon: '🏗️' },
{ step: '6', label: 'EC2 pulls and restarts', icon: '🚀' },
{ step: '7', label: 'Live on devopsbliss.online', icon: '✅' },
].map((s, i, arr) => (
<div key={i} className="flex items-center gap-2">
<div className="flex flex-col items-center gap-1.5 bg-surface border border-border rounded-xl px-4 py-3 text-center min-w-[100px]">
<span className="text-xl">{s.icon}</span>
<span className="text-[0.65rem] font-600 text-accent">Step {s.step}</span>
<span className="text-xs text-text2 leading-tight">{s.label}</span>
</div>
{i < arr.length - 1 && (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3d4257" strokeWidth="2"><polyline points="9 18 15 12 9 6"/></svg>
)}
</div>
))}
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Production-grade from week one</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Not a toy app. A real infrastructure story built by a DevOps engineer learning in public.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Built by a DevOps Engineer</h2>
<p className="text-sm text-text2 mb-2 max-w-lg mx-auto">
This platform was designed and built by Elizabeth Ikechukwu as part of a 10-week DevOps portfolio program through the Digital Witch Support Community, mentored by Oz.
</p>
<p className="text-xs text-text3 mb-8">AWS - Terraform - Docker - GitHub Actions - Node.js - PostgreSQL - React</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="https://github.com/elizabeth-ikechukwu/scalable-task-platform" target="_blank" rel="noopener noreferrer"
className="px-8 py-3.5 bg-surface hover:bg-surface2 text-text1 font-700 rounded-xl text-sm transition-all border border-border hover:border-accent/40 flex items-center justify-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>
View on GitHub
</a>
<a href="/register" className="px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Try the Platform
</a>
</div>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,105 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Never Miss Important Updates',
description: 'Receive intelligent real-time notifications for task assignments, project activity, approaching deadlines, mentions, and workflow updates. Stay informed without being overwhelmed.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 01-3.46 0"/>
</svg>
),
color: 'text-yellow-400',
bg: 'bg-yellow-400/10 border-yellow-400/20',
}
const CAPABILITIES = [
{ title: 'Real-Time Alerts', description: 'Get notified the moment a task is assigned to you, a deadline is approaching, or a teammate mentions you in a comment.', icon: '⚡' },
{ title: 'Deadline Reminders', description: 'Automatic reminders before task due dates. Never miss a deadline because you forgot to check.', icon: '⏰' },
{ title: 'Task Update Alerts', description: 'Know immediately when a task you own or follow is updated, commented on, or moved to a new status.', icon: '🔄' },
{ title: 'Mention Notifications', description: 'When someone tags you with @mention in a task comment, you get a direct notification with full context.', icon: '@' },
{ title: 'Notification Center', description: 'All your alerts in one place. Mark as read, filter by type, and manage your notification preferences from the sidebar.', icon: '🔔' },
{ title: 'Quiet Hours', description: 'Set quiet hours so you are not disturbed outside of work. TaskFlow respects your focus time.', icon: '🌙' },
]
const MOCK_NOTIFICATIONS = [
{ type: 'mention', title: 'You were mentioned', message: 'Tarak mentioned you in "SSM Parameter Store integration"', time: '2m ago', read: false, color: 'text-accent bg-accent-dim border-accent/20' },
{ type: 'deadline', title: 'Deadline approaching', message: '"Deploy to Kubernetes" is due in 2 days', time: '1h ago', read: false, color: 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20' },
{ type: 'completed', title: 'Task completed', message: '"Set up RDS with Terraform" was marked complete', time: '3h ago', read: true, color: 'text-green bg-green-dim border-green/20' },
{ type: 'assigned', title: 'Task assigned to you', message: '"Implement JWT authentication" was assigned to you', time: '5h ago', read: true, color: 'text-accent bg-accent-dim border-accent/20' },
]
export default function NotificationsFeature() {
return (
<FeaturePageLayout hero={HERO}>
{/* Notification center preview */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Live Preview</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">Your notification center</h2>
</div>
<div className="bg-surface border border-border rounded-2xl overflow-hidden max-w-xl mx-auto">
<div className="px-5 py-4 border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="font-display font-700 text-text1 text-sm">Notifications</h3>
<span className="bg-accent text-white text-[0.6rem] font-700 px-1.5 py-0.5 rounded-full">2</span>
</div>
<button className="text-xs font-600 text-accent">Mark all read</button>
</div>
<div className="p-3 flex flex-col gap-2">
{MOCK_NOTIFICATIONS.map((n, i) => (
<div key={i} className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${n.read ? 'bg-surface border-border opacity-60' : 'bg-surface2 border-border'}`}>
<div className={`w-7 h-7 rounded-full border flex items-center justify-center text-xs flex-shrink-0 ${n.color}`}>
{n.type === 'mention' ? '@' : n.type === 'deadline' ? '⏰' : n.type === 'completed' ? '✓' : '→'}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5">
<p className="text-xs font-600 text-text1">{n.title}</p>
{!n.read && <span className="w-1.5 h-1.5 rounded-full bg-accent flex-shrink-0" />}
</div>
<p className="text-[0.65rem] text-text2 leading-snug">{n.message}</p>
</div>
<span className="text-[0.6rem] text-text3 flex-shrink-0">{n.time}</span>
</div>
))}
</div>
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Stay informed without the noise</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Smart notifications that surface what matters and stay quiet when you need focus.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Stay on top of everything</h2>
<p className="text-sm text-text2 mb-8">Never miss a deadline, mention, or update that matters to your work.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Free
</a>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,101 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Centralized Project Workspaces',
description: 'Organize tasks, milestones, files, and team collaboration inside dedicated project environments designed for scalable operations and efficient delivery.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 7a2 2 0 012-2h4l2 3h9a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V7z"/>
</svg>
),
color: 'text-blue-400',
bg: 'bg-blue-400/10 border-blue-400/20',
}
const CAPABILITIES = [
{ title: 'Project Dashboards', description: 'Each project has its own dashboard showing task counts, progress, team members, and recent activity at a glance.', icon: '📊' },
{ title: 'Milestones', description: 'Define project milestones to track major checkpoints. See how much of the project is complete toward each milestone.', icon: '🏁' },
{ title: 'Timeline Tracking', description: 'View tasks on a timeline to spot bottlenecks, overlapping work, and critical path dependencies.', icon: '📅' },
{ title: 'Team Assignments', description: 'Assign team members to projects. Control who has access and what they can do within each project workspace.', icon: '👥' },
{ title: 'Shared Resources', description: 'Attach links, files, and documentation directly to a project so everything your team needs is in one place.', icon: '📁' },
{ title: 'Project Templates', description: 'Create reusable project templates for common workflows -- sprint cycles, feature launches, infrastructure setups, and more.', icon: '🗂️' },
]
const MOCK_PROJECTS = [
{ name: 'AWS Infrastructure', tasks: 12, done: 8, color: 'from-accent to-purple-500', initial: 'A' },
{ name: 'TaskFlow Backend API', tasks: 18, done: 14, color: 'from-green to-accent', initial: 'T' },
{ name: 'Kubernetes Migration', tasks: 7, done: 2, color: 'from-blue-500 to-accent', initial: 'K' },
{ name: 'Frontend UI Overhaul', tasks: 24, done: 20, color: 'from-yellow-500 to-green', initial: 'F' },
]
export default function ProjectWorkspaces() {
return (
<FeaturePageLayout hero={HERO}>
{/* Projects preview */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Live Preview</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">Your project workspaces</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-3xl mx-auto">
{MOCK_PROJECTS.map((p, i) => {
const progress = Math.round((p.done / p.tasks) * 100)
return (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200 group">
<div className="flex items-start justify-between mb-4">
<div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${p.color} flex items-center justify-center text-white font-700 text-sm`}>
{p.initial}
</div>
<span className="text-[0.65rem] font-600 text-accent bg-accent-dim border border-accent/20 px-2 py-0.5 rounded-full">Active</span>
</div>
<h3 className="font-display font-700 text-text1 text-sm mb-3 group-hover:text-accent transition-colors">{p.name}</h3>
<div className="flex items-center justify-between text-xs text-text3 mb-2">
<span>{p.done}/{p.tasks} tasks</span>
<span>{progress}%</span>
</div>
<div className="h-1.5 bg-border rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-accent to-green rounded-full transition-all" style={{ width: `${progress}%` }} />
</div>
</div>
)
})}
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">One workspace per project</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Keep every project organized, focused, and accessible to the right people.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Organize your work into projects</h2>
<p className="text-sm text-text2 mb-8">Create your first project workspace and start delivering work with clarity and focus.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Create Your First Project
</a>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,114 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Enterprise-Grade Security and Permissions',
description: 'Manage secure collaboration with role-based access control, workspace permissions, and protected infrastructure designed for growing organizations. Your data stays private by design.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0110 0v4"/>
</svg>
),
color: 'text-red-400',
bg: 'bg-red-400/10 border-red-400/20',
}
const CAPABILITIES = [
{ title: 'JWT Authentication', description: 'Every API request is authenticated with a JSON Web Token. Tokens expire after 7 days and are validated on every request.', icon: '🔑' },
{ title: 'Password Hashing', description: 'Passwords are hashed with bcrypt at cost factor 12 before storage. Plain text passwords are never stored or logged.', icon: '🔒' },
{ title: 'Role-Based Access', description: 'Three roles -- Admin, Member, Viewer -- control exactly what each user can see and do within a workspace.', icon: '👥' },
{ title: 'User Isolation', description: 'Every task, project, and notification is scoped to the authenticated user. No cross-user data leakage by design.', icon: '🛡️' },
{ title: 'SSM Parameter Store', description: 'Database credentials are stored in AWS SSM Parameter Store, not hardcoded in code or environment files on EC2.', icon: '🗝️' },
{ title: 'HTTPS Everywhere', description: 'All traffic is encrypted in transit via HTTPS. The backend never serves unencrypted HTTP in production.', icon: '🔐' },
]
const ROLES = [
{ role: 'Admin', create: true, edit: true, delete: true, invite: true, view: true },
{ role: 'Member', create: true, edit: true, delete: false, invite: false, view: true },
{ role: 'Viewer', create: false, edit: false, delete: false, invite: false, view: true },
]
const PERMISSIONS = ['Create Tasks', 'Edit Tasks', 'Delete Tasks', 'Invite Members', 'View Projects']
function Check({ val }) {
return val ? (
<div className="w-5 h-5 rounded-full bg-green-dim border border-green/30 flex items-center justify-center mx-auto">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>
</div>
) : (
<div className="w-5 h-5 rounded-full bg-red-dim border border-red/20 flex items-center justify-center mx-auto">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none" stroke="#ef4444" strokeWidth="2.5"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg>
</div>
)
}
export default function Security() {
return (
<FeaturePageLayout hero={HERO}>
{/* Permission matrix */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Role Matrix</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">Who can do what</h2>
</div>
<div className="overflow-x-auto max-w-2xl mx-auto">
<table className="w-full bg-surface border border-border rounded-xl overflow-hidden">
<thead>
<tr className="border-b border-border bg-surface2">
<th className="px-5 py-3 text-left text-xs font-700 text-text3 uppercase tracking-wider">Permission</th>
{ROLES.map(r => (
<th key={r.role} className="px-4 py-3 text-center text-xs font-700 text-text3 uppercase tracking-wider">{r.role}</th>
))}
</tr>
</thead>
<tbody>
{PERMISSIONS.map((perm, i) => (
<tr key={perm} className={`border-b border-border ${i % 2 === 0 ? '' : 'bg-surface2/30'}`}>
<td className="px-5 py-3 text-sm text-text2">{perm}</td>
<td className="px-4 py-3"><Check val={ROLES[0][Object.keys(ROLES[0])[i + 1]]} /></td>
<td className="px-4 py-3"><Check val={ROLES[1][Object.keys(ROLES[1])[i + 1]]} /></td>
<td className="px-4 py-3"><Check val={ROLES[2][Object.keys(ROLES[2])[i + 1]]} /></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Secure by design, not by default</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Security decisions were made at the architecture level -- not bolted on after the fact.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Your data is safe with TaskFlow</h2>
<p className="text-sm text-text2 mb-8">Built on AWS with JWT auth, bcrypt passwords, SSM secrets, and role-based access from day one.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Securely
</a>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,138 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Task Management Built for Productivity',
description: 'Create, organize, prioritize, and monitor tasks using flexible workflows designed for modern teams. Track deadlines, manage priorities, and maintain full visibility across projects from one centralized workspace.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
),
color: 'text-accent',
bg: 'bg-accent-dim border-accent/20',
}
const CAPABILITIES = [
{
title: 'Kanban Boards',
description: 'Visualize work in progress with drag-and-drop Kanban boards. Move tasks across columns -- To Do, In Progress, Review, Done -- and see your workflow at a glance.',
icon: '⬛',
},
{
title: 'Task Priorities',
description: 'Mark tasks as urgent, high, medium, or low priority. Filter and sort by priority to always work on what matters most.',
icon: '🔴',
},
{
title: 'Due Dates',
description: 'Set deadlines on individual tasks. Tasks approaching their due date are highlighted so nothing slips through the cracks.',
icon: '📅',
},
{
title: 'Labels and Tags',
description: 'Categorize tasks with color-coded labels. Group by feature, team, sprint, or any custom taxonomy that fits your workflow.',
icon: '🏷️',
},
{
title: 'Task Filtering',
description: 'Filter tasks by status, assignee, priority, label, or date range. Find exactly what you need in seconds across large task lists.',
icon: '🔍',
},
{
title: 'Activity Tracking',
description: 'Every task change is logged. See who created, updated, or completed a task and when -- giving you a full audit trail.',
icon: '📊',
},
]
const MOCK_TASKS = [
{ title: 'Set up RDS with Terraform', priority: 'high', status: 'done', label: 'Infrastructure' },
{ title: 'Configure OIDC GitHub Actions', priority: 'high', status: 'done', label: 'CI/CD' },
{ title: 'Implement JWT authentication', priority: 'urgent', status: 'in-progress', label: 'Backend' },
{ title: 'SSM Parameter Store integration', priority: 'medium', status: 'todo', label: 'Security' },
{ title: 'Build Analytics dashboard', priority: 'medium', status: 'todo', label: 'Frontend' },
{ title: 'Deploy to Kubernetes', priority: 'low', status: 'todo', label: 'DevOps' },
]
const PRIORITY_COLORS = {
urgent: 'text-red bg-red-dim border-red/20',
high: 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20',
medium: 'text-accent bg-accent-dim border-accent/20',
low: 'text-text3 bg-surface2 border-border',
}
const STATUS_COLORS = {
done: 'text-green bg-green-dim border-green/20',
'in-progress': 'text-accent bg-accent-dim border-accent/20',
todo: 'text-text3 bg-surface2 border-border',
}
export default function TaskManagement() {
return (
<FeaturePageLayout hero={HERO}>
{/* Interactive task list preview */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-10">
<p className="text-xs font-700 text-accent uppercase tracking-widest mb-2">Live Preview</p>
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight">See it in action</h2>
</div>
<div className="bg-surface border border-border rounded-2xl overflow-hidden max-w-3xl mx-auto">
<div className="flex items-center gap-2 px-4 py-3 border-b border-border bg-surface2">
<span className="w-2.5 h-2.5 rounded-full bg-red/60" />
<span className="w-2.5 h-2.5 rounded-full bg-yellow-400/60" />
<span className="w-2.5 h-2.5 rounded-full bg-green/60" />
<span className="ml-3 text-xs text-text3">devopsbliss.online/dashboard/tasks</span>
</div>
<div className="p-4 flex flex-col gap-2">
{MOCK_TASKS.map((task, i) => (
<div key={i} className="flex items-center gap-3 bg-surface2 border border-border rounded-lg px-3 py-2.5 hover:border-accent/30 transition-colors">
<div className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 ${task.status === 'done' ? 'bg-green border-green' : 'border-2 border-text3'}`}>
{task.status === 'done' && <svg width="8" height="8" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2.5"><polyline points="1.5,6 5,9.5 10.5,2.5"/></svg>}
</div>
<span className={`flex-1 text-xs font-500 ${task.status === 'done' ? 'line-through text-text3' : 'text-text1'}`}>{task.title}</span>
<span className={`text-[0.6rem] font-600 px-1.5 py-0.5 rounded-full border ${PRIORITY_COLORS[task.priority]}`}>{task.priority}</span>
<span className={`text-[0.6rem] font-600 px-1.5 py-0.5 rounded-full border hidden sm:block ${STATUS_COLORS[task.status]}`}>{task.status}</span>
<span className="text-[0.6rem] text-text3 hidden md:block">{task.label}</span>
</div>
))}
</div>
</div>
</div>
</section>
{/* Capabilities grid */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Everything you need to stay organized</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Built for engineers, product teams, and anyone who ships work consistently.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Start managing tasks the right way</h2>
<p className="text-sm text-text2 mb-8">Join TaskFlow and bring structure to your work from day one.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Get Started Free
</a>
</div>
</section>
</FeaturePageLayout>
)
}
@@ -0,0 +1,133 @@
import FeaturePageLayout from '../../components/FeaturePageLayout'
const HERO = {
category: 'Feature',
title: 'Collaborate Across Teams in Real Time',
description: 'Keep everyone aligned with shared workspaces, instant communication, collaborative task tracking, and centralized project coordination. Built for teams that move fast and need to stay in sync.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
</svg>
),
color: 'text-green',
bg: 'bg-green-dim border-green/20',
}
const CAPABILITIES = [
{ title: 'Team Workspaces', description: 'Create dedicated shared workspaces for each team or project. Everyone sees the same tasks, priorities, and progress in real time.', icon: '🏢' },
{ title: 'Activity Feed', description: 'A live feed of everything happening in your workspace -- task updates, completions, new members, and comments -- all in one timeline.', icon: '📡' },
{ title: 'Comments & Mentions', description: 'Leave comments directly on tasks. Mention teammates with @ to notify them instantly and keep conversations in context.', icon: '💬' },
{ title: 'Role-Based Access', description: 'Assign Admin, Member, or Viewer roles to control who can create, edit, or view tasks within your workspace.', icon: '🔐' },
{ title: 'Shared Boards', description: 'Every team member sees the same board. Full transparency across the team with no miscommunication about assignments.', icon: '📋' },
{ title: 'File Sharing', description: 'Attach files, links, and resources directly to tasks. Keep all project assets in one place alongside the work itself.', icon: '📎' },
]
const TEAM_MEMBERS = [
{ role: 'Admin', color: 'from-accent to-green', initial: 'A' },
{ role: 'Member', color: 'from-purple-500 to-accent', initial: 'B' },
{ role: 'Member', color: 'from-green to-blue-500', initial: 'C' },
{ role: 'Viewer', color: 'from-yellow-500 to-red', initial: 'D' },
]
const ACTIVITY = [
{ initial: 'A', action: 'completed', target: 'Review Q3 product roadmap', time: '2m ago', color: 'bg-green' },
{ initial: 'B', action: 'created', target: 'Design new onboarding flow', time: '15m ago', color: 'bg-accent' },
{ initial: 'C', action: 'commented', target: 'Launch email campaign', time: '1h ago', color: 'bg-purple-500' },
{ initial: 'D', action: 'assigned', target: 'Prepare performance review docs', time: '2h ago', color: 'bg-yellow-500' },
]
export default function TeamCollaboration() {
return (
<FeaturePageLayout hero={HERO}>
{/* Team preview */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
{/* Team members */}
<div className="bg-surface border border-border rounded-2xl overflow-hidden">
<div className="px-5 py-4 border-b border-border">
<h3 className="font-display font-700 text-text1 text-sm">Workspace Members</h3>
</div>
<div className="p-4 flex flex-col gap-2">
{TEAM_MEMBERS.map((m, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-surface2 transition-colors">
<div className={`w-8 h-8 rounded-full bg-gradient-to-br ${m.color} flex items-center justify-center text-white text-xs font-700 flex-shrink-0`}>
{m.initial}
</div>
<div className="flex-1">
<p className="text-sm font-600 text-text1">Team Member {m.initial}</p>
</div>
<span className={`text-[0.65rem] font-600 px-2 py-0.5 rounded-full border
${m.role === 'Admin' ? 'text-accent bg-accent-dim border-accent/20' :
m.role === 'Member' ? 'text-green bg-green-dim border-green/20' :
'text-text3 bg-surface2 border-border'}`}>
{m.role}
</span>
</div>
))}
</div>
</div>
{/* Activity feed */}
<div className="bg-surface border border-border rounded-2xl overflow-hidden">
<div className="px-5 py-4 border-b border-border">
<h3 className="font-display font-700 text-text1 text-sm">Live Activity Feed</h3>
</div>
<div className="p-4 flex flex-col gap-3">
{ACTIVITY.map((a, i) => (
<div key={i} className="flex items-start gap-3">
<div className={`w-7 h-7 rounded-full ${a.color} flex items-center justify-center text-white text-xs font-700 flex-shrink-0`}>
{a.initial}
</div>
<div className="flex-1">
<p className="text-xs text-text2 leading-snug">
<span className="text-text1 font-600">Member {a.initial} </span>
{a.action} <span className="text-accent">"{a.target}"</span>
</p>
</div>
<span className="text-[0.6rem] text-text3 flex-shrink-0">{a.time}</span>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Capabilities */}
<section className="py-16 border-b border-border">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Built for teams that move fast</h2>
<p className="text-sm text-text2 max-w-xl mx-auto">Everything your team needs to stay aligned without unnecessary meetings or status updates.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CAPABILITIES.map((cap, i) => (
<div key={i} className="bg-surface border border-border rounded-xl p-5 hover:bg-surface2 hover:-translate-y-1 transition-all duration-200">
<div className="text-2xl mb-3">{cap.icon}</div>
<h3 className="font-display font-700 text-text1 text-sm mb-2">{cap.title}</h3>
<p className="text-xs text-text2 leading-relaxed">{cap.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="font-display font-800 text-2xl text-text1 tracking-tight mb-3">Bring your team together</h2>
<p className="text-sm text-text2 mb-8">Invite your team to TaskFlow and start collaborating from one shared workspace.</p>
<a href="/register" className="inline-block px-8 py-3.5 bg-accent hover:bg-accent-hover text-white font-700 rounded-xl text-sm transition-all shadow-accent hover:-translate-y-0.5">
Invite Your Team
</a>
</div>
</section>
</FeaturePageLayout>
)
}