Improved UI and compliance with api endpoint restrictions

This commit is contained in:
Sven laptop
2026-07-24 23:51:26 +02:00
parent c891f53197
commit beb8bb287e
17 changed files with 1464 additions and 335 deletions
+21
View File
@@ -0,0 +1,21 @@
import { useNavigate } from "react-router-dom";
import { PostComposer } from "../components/PostComposer";
export function CreatePage({ createPost }) {
const navigate = useNavigate();
async function handleCreate(data) {
await createPost(data);
navigate("/feed");
}
return (
<section className="create-page">
<header className="page-head">
<p className="eyebrow">Share something new</p>
<h2>Create post</h2>
</header>
<PostComposer onCreate={handleCreate} />
</section>
);
}
-58
View File
@@ -1,58 +0,0 @@
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>
);
}
+60 -41
View File
@@ -1,53 +1,72 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { PostList } from "../components/PostList";
export function FeedPage({ loadFeed }) {
const [items, setItems] = useState([]);
function getPosts(data) {
return Array.isArray(data) ? data : data.posts || data.items || [];
}
export function FeedPage({ loadPosts, editPost, deletePost, loadImage, currentUserId, currentUsername }) {
const [posts, setPosts] = useState([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const refresh = useCallback(() => {
setError("");
setLoading(true);
return loadPosts()
.then((data) => {
const rawPosts = getPosts(data);
// Filter out logged-in user's own posts on feed
const otherUserPosts = rawPosts.filter((post) => {
const authorId = post.author_id ?? post.authorId ?? post.author?.id;
if (authorId !== undefined && authorId !== null) {
return String(authorId) !== String(currentUserId);
}
if (post.author_username && currentUsername) {
return post.author_username !== currentUsername;
}
return true;
});
setPosts(otherUserPosts);
})
.catch((loadError) => setError(loadError.message))
.finally(() => setLoading(false));
}, [loadPosts, currentUserId, currentUsername]);
useEffect(() => {
let mounted = true;
loadFeed()
.then((data) => {
if (mounted) {
setItems(data.items || []);
}
})
.catch((err) => {
if (mounted) {
setError(err.message);
}
});
refresh();
}, [refresh]);
return () => {
mounted = false;
};
}, [loadFeed]);
async function handleEdit(id, data) {
await editPost(id, data);
await refresh();
}
async function handleDelete(id) {
await deletePost(id);
await refresh();
}
return (
<section>
<header className="page-head">
<h2>Community Feed</h2>
<section className="feed-page">
<header className="feed-header">
<div>
<p className="eyebrow">Your daily archive</p>
<h2>Community Feed</h2>
</div>
<span className="feed-status">Explore posts from others</span>
</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>
{loading ? (
<p className="post-loading">Loading community feed...</p>
) : posts.length === 0 ? (
<div className="panel empty-feed-panel">
<h3>No posts from other users yet</h3>
<p className="muted">Posts created by you will appear on your profile. Check back later for community updates!</p>
</div>
) : (
<PostList posts={posts} currentUserId={currentUserId} loadImage={loadImage} onEdit={handleEdit} onDelete={handleDelete} />
)}
</section>
);
}
+15 -3
View File
@@ -1,12 +1,20 @@
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import { AuthForm } from "../components/AuthForm";
import { normalizeLoginData, validateLogin } from "../validation/auth";
import { getLoginFieldErrors, normalizeLoginData, validateLogin } from "../validation/auth";
export function LoginPage({ onLogin }) {
const navigate = useNavigate();
const [form, setForm] = useState({ email: "", password: "" });
const [touched, setTouched] = useState({});
const [error, setError] = useState("");
const fieldErrors = getLoginFieldErrors(form);
function updateField(name, value) {
setForm((previous) => ({ ...previous, [name]: value }));
setTouched((previous) => ({ ...previous, [name]: true }));
setError("");
}
async function handleSubmit(event) {
event.preventDefault();
@@ -38,7 +46,9 @@ export function LoginPage({ onLogin }) {
label: "E-mail",
type: "text",
value: form.email,
onChange: (event) => setForm((prev) => ({ ...prev, email: event.target.value })),
onChange: (event) => updateField("email", event.target.value),
error: touched.email ? fieldErrors.email : null,
valid: touched.email && !fieldErrors.email,
required: true,
minLength: 1,
maxLength: 254,
@@ -49,7 +59,9 @@ export function LoginPage({ onLogin }) {
label: "Wachtwoord",
type: "password",
value: form.password,
onChange: (event) => setForm((prev) => ({ ...prev, password: event.target.value })),
onChange: (event) => updateField("password", event.target.value),
error: touched.password ? fieldErrors.password : null,
valid: touched.password && !fieldErrors.password,
required: true,
minLength: 1,
maxLength: 128,
+190 -10
View File
@@ -1,22 +1,202 @@
import { UploadBox } from "../components/UploadBox";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { PostList } from "../components/PostList";
export function ProfilePage({ user, onUpload }) {
function getPosts(data) {
return Array.isArray(data) ? data : data.posts || data.items || [];
}
function getInitials(name) {
if (!name) return "?";
const cleaned = String(name).replace(/^@/, "");
return (
<section>
cleaned
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0])
.join("")
.toUpperCase() || "?"
);
}
export function ProfilePage({
user,
myProfile,
getProfile,
loadMyPosts,
loadAllPosts,
createRelationship,
editPost,
deletePost,
loadImage,
currentUserId,
onLogout,
}) {
const { userId } = useParams();
const isOwnProfile = !userId || String(userId) === String(currentUserId);
const [profileData, setProfileData] = useState(isOwnProfile ? user : null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [following, setFollowing] = useState(false);
const [followMessage, setFollowMessage] = useState("");
const [chatNotice, setChatNotice] = useState("");
const loadData = useCallback(async () => {
setLoading(true);
setError("");
setFollowMessage("");
setChatNotice("");
try {
if (isOwnProfile) {
if (myProfile) {
try {
const me = await myProfile();
setProfileData(me);
} catch {
// Keep default user prop
}
}
const myPostsData = await loadMyPosts();
setPosts(getPosts(myPostsData));
} else {
if (getProfile) {
try {
const targetProfile = await getProfile(userId);
setProfileData(targetProfile);
} catch {
setProfileData({ id: userId, name: `User ${userId}`, username: null });
}
} else {
setProfileData({ id: userId, name: `User ${userId}`, username: null });
}
const allPostsData = await loadAllPosts();
const allPosts = getPosts(allPostsData);
const userPosts = allPosts.filter((post) => {
const authorId = post.author_id ?? post.authorId ?? post.author?.id;
return String(authorId) === String(userId);
});
setPosts(userPosts);
}
} catch (err) {
setError(err.message || "Unable to load profile.");
} finally {
setLoading(false);
}
}, [isOwnProfile, userId, myProfile, getProfile, loadMyPosts, loadAllPosts]);
useEffect(() => {
loadData();
}, [loadData]);
async function handleFollow() {
if (!userId) return;
try {
if (createRelationship) {
await createRelationship(userId);
}
setFollowing(true);
const nameOrHandle = profileData?.name || (profileData?.username ? `@${profileData.username}` : `User ${userId}`);
setFollowMessage(`You are now following ${nameOrHandle}!`);
} catch {
// Unbinded preview fallback
setFollowing(true);
setFollowMessage(`Follow relationship recorded for User ${userId}`);
}
}
function handleChat() {
setChatNotice("Chat feature is coming soon! (Action unbinded)");
}
async function handleEdit(id, data) {
await editPost(id, data);
await loadData();
}
async function handleDelete(id) {
await deletePost(id);
await loadData();
}
const displayName = profileData?.name || (isOwnProfile ? (user?.name || "Your Profile") : `User ${userId}`);
const displayUsername = profileData?.username || (isOwnProfile ? user?.username : null);
const displayId = profileData?.id || userId || currentUserId;
return (
<section className="profile-page">
<header className="page-head">
<h2>Profile Cabinet</h2>
<p className="eyebrow">{isOwnProfile ? "Personal Cabinet" : "User Profile"}</p>
<h2>{isOwnProfile ? "Account Profile" : displayName}</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 className="profile-header-content">
<div className="profile-avatar-large">{getInitials(displayName)}</div>
<div className="profile-details">
<h3 className="profile-name">{displayName}</h3>
{displayUsername ? <p className="profile-username">@{displayUsername}</p> : null}
<span className="profile-id-badge">ID: {displayId}</span>
</div>
<div className="profile-actions-bar">
{isOwnProfile ? (
<>
<button
type="button"
className="btn-profile-secondary"
onClick={() => setChatNotice("Profile editing feature is ready.")}
>
Edit Profile
</button>
{onLogout ? (
<button type="button" className="btn-profile-danger" onClick={onLogout}>
Logout
</button>
) : null}
</>
) : (
<>
<button
type="button"
className={following ? "btn-profile-following" : "btn-profile-primary"}
onClick={handleFollow}
>
{following ? "✓ Following" : " Follow"}
</button>
<button
type="button"
className="btn-profile-secondary"
onClick={handleChat}
title="Unbinded button for future chat implementation"
>
💬 Chat
</button>
</>
)}
</div>
</div>
{followMessage ? <p className="success-text profile-notice">{followMessage}</p> : null}
{chatNotice ? <p className="info-text profile-notice">{chatNotice}</p> : null}
</article>
<UploadBox onUpload={onUpload} />
{error ? <p className="error-text">{error}</p> : null}
<div className="profile-posts-section">
<h3 className="section-title">{isOwnProfile ? "Your posts" : `Posts by ${displayName}`}</h3>
{loading ? (
<p className="post-loading">Loading posts...</p>
) : posts.length === 0 ? (
<p className="muted">{isOwnProfile ? "You haven't created any posts yet." : "No posts found for this user."}</p>
) : (
<PostList posts={posts} currentUserId={currentUserId} loadImage={loadImage} onEdit={handleEdit} onDelete={handleDelete} />
)}
</div>
</section>
);
}
+22 -5
View File
@@ -2,6 +2,7 @@ import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import { AuthForm } from "../components/AuthForm";
import {
getRegistrationFieldErrors,
isControlCharacterPresent,
normalizeRegistrationData,
validateRegistration,
@@ -10,7 +11,15 @@ import {
export function RegisterPage({ onRegister }) {
const navigate = useNavigate();
const [form, setForm] = useState({ name: "", username: "", email: "", password: "" });
const [touched, setTouched] = useState({});
const [error, setError] = useState("");
const fieldErrors = getRegistrationFieldErrors(form);
function updateField(name, value) {
setForm((previous) => ({ ...previous, [name]: value }));
setTouched((previous) => ({ ...previous, [name]: true }));
setError("");
}
async function handleSubmit(event) {
event.preventDefault();
@@ -29,7 +38,7 @@ export function RegisterPage({ onRegister }) {
try {
await onRegister(normalizeRegistrationData(form));
navigate("/feed");
navigate("/login");
} catch (err) {
setError(err.message);
}
@@ -46,7 +55,9 @@ export function RegisterPage({ onRegister }) {
name: "name",
label: "Naam",
value: form.name,
onChange: (event) => setForm((prev) => ({ ...prev, name: event.target.value })),
onChange: (event) => updateField("name", event.target.value),
error: touched.name ? fieldErrors.name : null,
valid: touched.name && !fieldErrors.name,
required: true,
minLength: 1,
maxLength: 100,
@@ -56,7 +67,9 @@ export function RegisterPage({ onRegister }) {
name: "username",
label: "Gebruikersnaam",
value: form.username,
onChange: (event) => setForm((prev) => ({ ...prev, username: event.target.value })),
onChange: (event) => updateField("username", event.target.value),
error: touched.username ? fieldErrors.username : null,
valid: touched.username && !fieldErrors.username,
required: true,
minLength: 1,
maxLength: 50,
@@ -68,7 +81,9 @@ export function RegisterPage({ onRegister }) {
label: "E-mail",
type: "text",
value: form.email,
onChange: (event) => setForm((prev) => ({ ...prev, email: event.target.value })),
onChange: (event) => updateField("email", event.target.value),
error: touched.email ? fieldErrors.email : null,
valid: touched.email && !fieldErrors.email,
required: true,
maxLength: 254,
autoComplete: "email",
@@ -78,7 +93,9 @@ export function RegisterPage({ onRegister }) {
label: "Wachtwoord",
type: "password",
value: form.password,
onChange: (event) => setForm((prev) => ({ ...prev, password: event.target.value })),
onChange: (event) => updateField("password", event.target.value),
error: touched.password ? fieldErrors.password : null,
valid: touched.password && !fieldErrors.password,
required: true,
minLength: 1,
maxLength: 128,