refactor: everything
This commit is contained in:
72
src/pages/Login.jsx
Normal file
72
src/pages/Login.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState, useContext } from 'react';
|
||||
import { AuthContext } from '../AuthContext';
|
||||
import { inputCls, labelCls } from '../constants';
|
||||
|
||||
export default function Login({ onLoginSuccess }) {
|
||||
const [formData, setFormData] = useState({ email: '', password: '' });
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useContext(AuthContext);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
const result = await login(formData.email, formData.password);
|
||||
if (result.success) {
|
||||
onLoginSuccess?.();
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="text-5xl mb-3">☕</div>
|
||||
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] mb-1">Brew Journal</h1>
|
||||
<p className="text-sm text-[#9C8B7A]">Sign in to your coffee logbook</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-[rgba(180,64,64,0.08)] border border-[rgba(180,64,64,0.2)] text-[#B44040] text-sm px-4 py-3 rounded-lg mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className={labelCls}>Email</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Password</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold tracking-wide cursor-pointer transition-opacity mt-1"
|
||||
style={{ opacity: loading ? 0.6 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/pages/ProfilePage.jsx
Normal file
73
src/pages/ProfilePage.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useContext } from "react";
|
||||
import { AuthContext } from "../AuthContext";
|
||||
|
||||
export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus }) {
|
||||
const { logout } = useContext(AuthContext);
|
||||
return (
|
||||
<div>
|
||||
{/* Avatar & Name */}
|
||||
<div className="flex flex-col items-center pt-6 pb-8">
|
||||
<div className="w-20 h-20 rounded-full bg-[#2C1810] flex items-center justify-center text-3xl text-[#FAF6F1] font-serif font-bold mb-3">
|
||||
{user?.username?.[0]?.toUpperCase() || "☕"}
|
||||
</div>
|
||||
<div className="font-serif text-xl font-semibold text-[#2C1810]">{user?.username}</div>
|
||||
<div className="text-sm text-[#9C8B7A] mt-0.5">{user?.email}</div>
|
||||
{showSyncedStatus && (
|
||||
<div className={`flex items-center gap-1.5 text-[11px] font-medium px-3 py-1.5 rounded-full mt-3 transition-all ${isOnline ? "text-[#4A7C59] bg-[rgba(74,124,89,0.1)]" : "text-[#B44040] bg-[rgba(180,64,64,0.1)]"}`}>
|
||||
<span className={`text-[10px] ${syncing ? "animate-sync-pulse" : ""}`}>●</span>
|
||||
<span>{isOnline ? (syncing ? "Syncing…" : "All data synced") : "Offline — saved locally"}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account section */}
|
||||
<div className="mb-6">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] mb-2 px-1">Account</div>
|
||||
<div className="bg-white rounded-2xl border border-[#E8DFD3] overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[#F3EDE4]">
|
||||
<span className="text-lg">👤</span>
|
||||
<div>
|
||||
<div className="text-xs text-[#9C8B7A]">Username</div>
|
||||
<div className="text-sm font-medium text-[#2C1810]">{user?.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<span className="text-lg">✉️</span>
|
||||
<div>
|
||||
<div className="text-xs text-[#9C8B7A]">Email</div>
|
||||
<div className="text-sm font-medium text-[#2C1810]">{user?.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App info */}
|
||||
<div className="mb-6">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] mb-2 px-1">App</div>
|
||||
<div className="bg-white rounded-2xl border border-[#E8DFD3] overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-3.5 border-b border-[#F3EDE4]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">☕</span>
|
||||
<div className="text-sm font-medium text-[#2C1810]">Brew Journal</div>
|
||||
</div>
|
||||
<div className="text-xs text-[#9C8B7A] font-mono">{__APP_VERSION__}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<span className="text-lg">🗄️</span>
|
||||
<div>
|
||||
<div className="text-xs text-[#9C8B7A]">Storage</div>
|
||||
<div className="text-sm font-medium text-[#2C1810]">Local + PostgreSQL</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign out */}
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
className="w-full py-3.5 border border-[#B44040] rounded-2xl text-sm font-semibold text-[#B44040] bg-transparent cursor-pointer hover:bg-[rgba(180,64,64,0.05)] transition-colors">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
src/pages/Register.jsx
Normal file
113
src/pages/Register.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useState, useContext } from 'react';
|
||||
import { AuthContext } from '../AuthContext';
|
||||
import { inputCls, labelCls } from '../constants';
|
||||
|
||||
export default function Register({ onRegisterSuccess }) {
|
||||
const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' });
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register } = useContext(AuthContext);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
if (formData.password.length < 6) {
|
||||
setError('Password must be at least 6 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const result = await register(formData.username, formData.email, formData.password);
|
||||
if (result.success) {
|
||||
setSuccess(result.message);
|
||||
setFormData({ username: '', email: '', password: '', confirmPassword: '' });
|
||||
onRegisterSuccess?.();
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="text-5xl mb-3">🫘</div>
|
||||
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] mb-1">Create Account</h1>
|
||||
<p className="text-sm text-[#9C8B7A]">Start tracking your coffee journey</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-[rgba(180,64,64,0.08)] border border-[rgba(180,64,64,0.2)] text-[#B44040] text-sm px-4 py-3 rounded-lg mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="bg-[rgba(74,124,89,0.08)] border border-[rgba(74,124,89,0.2)] text-[#4A7C59] text-sm px-4 py-3 rounded-lg mb-4">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className={labelCls}>Username</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="text"
|
||||
placeholder="coffeelover"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Email</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Password</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Confirm Password</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold tracking-wide mt-1 cursor-pointer transition-opacity"
|
||||
style={{ opacity: loading ? 0.6 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
{loading ? 'Creating account…' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user