v3: follows, notifications, invite-only registration, unread markers

This commit is contained in:
khannurien
2026-03-21 18:42:47 +00:00
parent 7c098e7c4c
commit 608c6bc6a8
55 changed files with 4743 additions and 884 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { Link, useNavigate, useParams } from "react-router";
import { API_URL } from "../config/api.ts";
import type { PlaylistWithDumps, RawPlaylistWithDumps } from "../model.ts";
import { deserializePlaylistWithDumps } from "../model.ts";
@@ -12,6 +12,7 @@ import { PageError } from "../components/PageError.tsx";
import { ConfirmModal } from "../components/ConfirmModal.tsx";
import { ImagePicker } from "../components/ImagePicker.tsx";
import { Markdown } from "../components/Markdown.tsx";
import { FollowPlaylistButton } from "../components/FollowButton.tsx";
type LoadState =
| { status: "loading" }
@@ -356,7 +357,6 @@ export function PlaylistDetail() {
setEditOpen(true);
};
const handleEditSave = async () => {
if (!playlistId || state.status !== "loaded") return;
setEditSaving(true);
@@ -392,7 +392,9 @@ export function PlaylistDetail() {
const handleDelete = async () => {
if (!playlistId) return;
await authFetch(`${API_URL}/api/playlists/${playlistId}`, { method: "DELETE" });
await authFetch(`${API_URL}/api/playlists/${playlistId}`, {
method: "DELETE",
});
navigate("/");
};
@@ -460,15 +462,58 @@ export function PlaylistDetail() {
<div className="playlist-detail-content">
{editOpen
? (
<input
type="text"
className="playlist-edit-input"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
autoFocus
/>
<div className="playlist-detail-title-row">
<input
type="text"
className="playlist-edit-input"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
autoFocus
/>
<button
type="button"
className="btn-primary"
disabled={editSaving}
onClick={handleEditSave}
>
{editSaving ? "Saving…" : "Save"}
</button>
<button
type="button"
className="form-cancel"
onClick={() => setEditOpen(false)}
>
Cancel
</button>
<button
type="button"
className="btn-danger"
onClick={() => setConfirmDelete(true)}
>
Delete
</button>
</div>
)
: <h1 className="playlist-detail-title">{playlist.title}</h1>}
: (
<div className="playlist-detail-title-row">
<h1 className="playlist-detail-title">{playlist.title}</h1>
{!isOwner && (
<FollowPlaylistButton
targetPlaylistId={playlist.id}
isPublic={playlist.isPublic}
/>
)}
{isOwner && (
<button
type="button"
className="playlist-edit-btn"
onClick={openEdit}
>
Edit
</button>
)}
</div>
)}
{editOpen
? (
@@ -516,6 +561,14 @@ export function PlaylistDetail() {
>
{playlist.isPublic ? "public" : "private"}
</span>
{playlist.ownerUsername && (
<Link
to={`/users/${playlist.ownerUsername}`}
className="playlist-detail-owner"
>
@{playlist.ownerUsername}
</Link>
)}
<time
dateTime={playlist.createdAt.toISOString()}
title={playlist.createdAt.toLocaleString()}
@@ -527,47 +580,6 @@ export function PlaylistDetail() {
</div>
{editError && <p className="form-error">{editError}</p>}
</div>
{isOwner && (
<div className="playlist-header-actions">
{editOpen
? (
<>
<button
type="button"
className="btn-primary"
disabled={editSaving}
onClick={handleEditSave}
>
{editSaving ? "Saving…" : "Save"}
</button>
<button
type="button"
className="btn-secondary"
onClick={() => setEditOpen(false)}
>
Cancel
</button>
<button
type="button"
className="btn-danger"
onClick={() => setConfirmDelete(true)}
>
Delete
</button>
</>
)
: (
<button
type="button"
className="playlist-edit-btn"
onClick={openEdit}
>
Edit
</button>
)}
</div>
)}
</div>
</div>