First commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user