Improved UI and compliance with api endpoint restrictions
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user