First commit

This commit is contained in:
Sven laptop
2026-07-24 21:55:46 +02:00
commit c891f53197
27 changed files with 2741 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import { useEffect, useState } from "react";
export function DiscoverPage({ loadUsers, sendFriendRequest }) {
const [users, setUsers] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
let mounted = true;
loadUsers()
.then((data) => {
if (mounted) {
setUsers(data.users || []);
}
})
.catch((err) => {
if (mounted) {
setError(err.message);
}
});
return () => {
mounted = false;
};
}, [loadUsers]);
async function handleAdd(userId) {
const result = await sendFriendRequest(userId);
if (result.status === "pending") {
setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, relationship: "pending" } : u)));
}
}
return (
<section>
<header className="page-head">
<h2>Discover People</h2>
</header>
{error ? <p className="error-text">{error}</p> : null}
<div className="grid">
{users.map((user) => (
<article className="panel user-card" key={user.id}>
<img src={user.avatarUrl} alt={user.name} />
<h3>{user.name}</h3>
<p>@{user.username}</p>
<button
type="button"
disabled={user.relationship !== "none"}
onClick={() => handleAdd(user.id)}
>
{user.relationship === "none" ? "Add friend" : user.relationship}
</button>
</article>
))}
</div>
</section>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
export function FeedPage({ loadFeed }) {
const [items, setItems] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
let mounted = true;
loadFeed()
.then((data) => {
if (mounted) {
setItems(data.items || []);
}
})
.catch((err) => {
if (mounted) {
setError(err.message);
}
});
return () => {
mounted = false;
};
}, [loadFeed]);
return (
<section>
<header className="page-head">
<h2>Community Feed</h2>
</header>
{error ? <p className="error-text">{error}</p> : null}
<div className="post-list">
{items.map((item) => (
<article key={item.id} className="panel post-card">
<div className="post-top">
<img src={item.author?.avatarUrl} alt={item.author?.name} />
<div>
<p>{item.author?.name}</p>
<span>@{item.author?.username}</span>
</div>
</div>
<p>{item.content}</p>
<button type="button" className="ghost-btn">
Like ({item.likes})
</button>
</article>
))}
</div>
</section>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { Link } from "react-router-dom";
export function LandingPage() {
return (
<div className="landing">
<div className="hero-noise" />
<section className="hero-card">
<p className="kicker">FILING CABINET</p>
<h1>Een sociaal platform voor makers, netwerken en verhalen.</h1>
<p>
Gebaseerd op een editorial social stijl: duidelijke kaarten, visuele ritme en focus op profiel en community.
</p>
<div className="hero-actions">
<Link to="/register" className="cta primary">
Start nu
</Link>
<Link to="/login" className="cta secondary">
Inloggen
</Link>
</div>
</section>
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import { AuthForm } from "../components/AuthForm";
import { normalizeLoginData, validateLogin } from "../validation/auth";
export function LoginPage({ onLogin }) {
const navigate = useNavigate();
const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState("");
async function handleSubmit(event) {
event.preventDefault();
setError("");
const validationError = validateLogin(form);
if (validationError) {
setError(validationError);
return;
}
try {
await onLogin(normalizeLoginData(form));
navigate("/feed");
} catch (err) {
setError(err.message);
}
}
return (
<div className="auth-page">
<AuthForm
title="Inloggen"
submitLabel="Ga naar je cabinet"
onSubmit={handleSubmit}
fields={[
{
name: "email",
label: "E-mail",
type: "text",
value: form.email,
onChange: (event) => setForm((prev) => ({ ...prev, email: event.target.value })),
required: true,
minLength: 1,
maxLength: 254,
autoComplete: "email",
},
{
name: "password",
label: "Wachtwoord",
type: "password",
value: form.password,
onChange: (event) => setForm((prev) => ({ ...prev, password: event.target.value })),
required: true,
minLength: 1,
maxLength: 128,
autoComplete: "current-password",
},
]}
footer={<p>Geen account? <Link to="/register">Meld je aan</Link></p>}
/>
{error ? <p className="error-text">{error}</p> : null}
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { UploadBox } from "../components/UploadBox";
export function ProfilePage({ user, onUpload }) {
return (
<section>
<header className="page-head">
<h2>Profile Cabinet</h2>
</header>
<article className="panel profile-card">
<img src={user.avatarUrl} alt={user.name} className="avatar-large" />
<div>
<h3>{user.name}</h3>
<p>@{user.username}</p>
<p className="muted">{user.bio}</p>
</div>
</article>
<UploadBox onUpload={onUpload} />
</section>
);
}
+94
View File
@@ -0,0 +1,94 @@
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import { AuthForm } from "../components/AuthForm";
import {
isControlCharacterPresent,
normalizeRegistrationData,
validateRegistration,
} from "../validation/auth";
export function RegisterPage({ onRegister }) {
const navigate = useNavigate();
const [form, setForm] = useState({ name: "", username: "", email: "", password: "" });
const [error, setError] = useState("");
async function handleSubmit(event) {
event.preventDefault();
setError("");
const validationError = validateRegistration(form);
if (validationError) {
setError(validationError);
return;
}
if ([form.name, form.username, form.email].some(isControlCharacterPresent) || isControlCharacterPresent(form.password)) {
setError("Input contains unsupported control characters.");
return;
}
try {
await onRegister(normalizeRegistrationData(form));
navigate("/feed");
} catch (err) {
setError(err.message);
}
}
return (
<div className="auth-page">
<AuthForm
title="Account aanmaken"
submitLabel="Maak account"
onSubmit={handleSubmit}
fields={[
{
name: "name",
label: "Naam",
value: form.name,
onChange: (event) => setForm((prev) => ({ ...prev, name: event.target.value })),
required: true,
minLength: 1,
maxLength: 100,
autoComplete: "name",
},
{
name: "username",
label: "Gebruikersnaam",
value: form.username,
onChange: (event) => setForm((prev) => ({ ...prev, username: event.target.value })),
required: true,
minLength: 1,
maxLength: 50,
title: "Gebruik 1-50 tekens zonder spaties.",
autoComplete: "username",
},
{
name: "email",
label: "E-mail",
type: "text",
value: form.email,
onChange: (event) => setForm((prev) => ({ ...prev, email: event.target.value })),
required: true,
maxLength: 254,
autoComplete: "email",
},
{
name: "password",
label: "Wachtwoord",
type: "password",
value: form.password,
onChange: (event) => setForm((prev) => ({ ...prev, password: event.target.value })),
required: true,
minLength: 1,
maxLength: 128,
title: "Gebruik minimaal 12 tekens met een kleine letter, hoofdletter, cijfer en speciaal teken.",
autoComplete: "new-password",
},
]}
footer={<p>Al een account? <Link to="/login">Inloggen</Link></p>}
/>
{error ? <p className="error-text">{error}</p> : null}
</div>
);
}