v3: added opengraph support to the app, wrote README instructions incl. a Docker image
This commit is contained in:
@@ -24,9 +24,9 @@ import { GlobalPlayer } from "./components/GlobalPlayer.tsx";
|
||||
import "./App.css";
|
||||
|
||||
function AppRoutes() {
|
||||
const { token, user } = useAuth();
|
||||
const { token, user, logout } = useAuth();
|
||||
return (
|
||||
<WSProvider token={token} userId={user?.id ?? null}>
|
||||
<WSProvider token={token} userId={user?.id ?? null} onForceLogout={logout}>
|
||||
<FollowProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
@@ -29,7 +29,8 @@ export function JournalCard(
|
||||
) {
|
||||
const navigate = useNavigate();
|
||||
const { play } = useContext(PlayerContext);
|
||||
const unread = !isOwner && isRecent(dump.createdAt) && !isDumpVisited(dump.id);
|
||||
const unread = !isOwner && isRecent(dump.createdAt) &&
|
||||
!isDumpVisited(dump.id);
|
||||
|
||||
function handleNavigate() {
|
||||
markDumpVisited(dump.id);
|
||||
@@ -73,8 +74,7 @@ export function JournalCard(
|
||||
</Tooltip>
|
||||
{dump.commentCount > 0 && (
|
||||
<span>
|
||||
{dump.commentCount}{" "}
|
||||
{dump.commentCount === 1 ? "comment" : "comments"}
|
||||
{dump.commentCount} {dump.commentCount === 1 ? "comment" : "comments"}
|
||||
</span>
|
||||
)}
|
||||
{dump.isPrivate && isOwner && (
|
||||
@@ -145,7 +145,10 @@ export function JournalCard(
|
||||
|
||||
if (tier === "medium") {
|
||||
return (
|
||||
<li className="journal-card journal-card--medium" onClick={handleNavigate}>
|
||||
<li
|
||||
className="journal-card journal-card--medium"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
<div className="journal-card-inner">
|
||||
<div className="journal-card-icon">
|
||||
{thumbnailUrl
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
// include type declarations from the package vite/client
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
const apiProtocol = import.meta.env.VITE_API_PROTOCOL || "http";
|
||||
const serverHost = import.meta.env.VITE_SERVER_HOST || "localhost";
|
||||
const serverPort = import.meta.env.VITE_SERVER_PORT || "8000";
|
||||
// In dev (Vite dev server), the frontend and API run on different ports, so we
|
||||
// need an absolute URL. VITE_API_* vars can override the defaults.
|
||||
//
|
||||
// In prod (same container), the frontend is served by the API server itself, so
|
||||
// both share the same origin. We use relative URLs ("") so no build-time
|
||||
// configuration is needed and the image works on any domain.
|
||||
const apiHostname = import.meta.env.VITE_API_HOSTNAME;
|
||||
|
||||
export const API_URL = `${apiProtocol}://${serverHost}:${serverPort}`;
|
||||
export const WS_URL = API_URL.replace(/^http/, "ws");
|
||||
export const API_URL: string = apiHostname
|
||||
? `${import.meta.env.VITE_API_PROTOCOL || "http"}://${apiHostname}:${
|
||||
import.meta.env.VITE_API_PORT || "8000"
|
||||
}`
|
||||
: import.meta.env.DEV
|
||||
? "http://localhost:8000"
|
||||
: "";
|
||||
|
||||
export const WS_URL: string = API_URL
|
||||
? API_URL.replace(/^http/, "ws")
|
||||
: `${location.protocol.replace("http", "ws")}//${location.host}`;
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const NOTIFICATIONS_PAGE_SIZE = 30;
|
||||
|
||||
@@ -35,6 +35,7 @@ interface WSProviderProps {
|
||||
children: ReactNode;
|
||||
token: string | null;
|
||||
userId: string | null;
|
||||
onForceLogout?: () => void;
|
||||
}
|
||||
|
||||
const MAX_BACKOFF = 30_000;
|
||||
@@ -54,7 +55,9 @@ function parseWSMessage(data: string): IncomingWSMessage | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function WSProvider({ children, token, userId }: WSProviderProps) {
|
||||
export function WSProvider(
|
||||
{ children, token, userId, onForceLogout }: WSProviderProps,
|
||||
) {
|
||||
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
|
||||
const [voteCounts, setVoteCounts] = useState<Record<string, number>>({});
|
||||
const [myVotes, setMyVotes] = useState<Set<string>>(new Set());
|
||||
@@ -264,6 +267,10 @@ export function WSProvider({ children, token, userId }: WSProviderProps) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "force_logout":
|
||||
onForceLogout?.();
|
||||
break;
|
||||
|
||||
case "error":
|
||||
// On error, revert any pending optimistic update for the affected dump
|
||||
// (the revert timeout will handle it)
|
||||
|
||||
@@ -396,6 +396,10 @@ export interface WSErrorMessage {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface WSForceLogoutMessage {
|
||||
type: "force_logout";
|
||||
}
|
||||
|
||||
export type IncomingWSMessage =
|
||||
| WSPingMessage
|
||||
| WSWelcomeMessage
|
||||
@@ -414,7 +418,8 @@ export type IncomingWSMessage =
|
||||
| WSCommentUpdatedMessage
|
||||
| WSCommentDeletedMessage
|
||||
| WSNotificationCreatedMessage
|
||||
| WSErrorMessage;
|
||||
| WSErrorMessage
|
||||
| WSForceLogoutMessage;
|
||||
|
||||
/**
|
||||
* WebSocket messages — client → server (outgoing)
|
||||
|
||||
@@ -90,7 +90,7 @@ function FollowedSubFeed({
|
||||
dump={dump}
|
||||
voteCount={voteCounts[dump.id] ?? dump.voteCount}
|
||||
voted={myVotes.has(dump.id)}
|
||||
canVote={true}
|
||||
canVote
|
||||
castVote={castVote}
|
||||
removeVote={removeVote}
|
||||
isOwner={user.id === dump.userId}
|
||||
@@ -217,7 +217,7 @@ export function FollowedFeed({
|
||||
})
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
// Scroll save
|
||||
@@ -324,9 +324,7 @@ export function FollowedFeed({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${
|
||||
section === "playlists" ? " active" : ""
|
||||
}`}
|
||||
className={`feed-sort-btn${section === "playlists" ? " active" : ""}`}
|
||||
onClick={() => setSection("playlists")}
|
||||
>
|
||||
From playlists
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { ErrorCard } from "../../components/ErrorCard.tsx";
|
||||
import { JournalCard, type JournalTier } from "../../components/JournalCard.tsx";
|
||||
import {
|
||||
JournalCard,
|
||||
type JournalTier,
|
||||
} from "../../components/JournalCard.tsx";
|
||||
import { hotScore } from "../../utils/hotScore.ts";
|
||||
import type { MainFeedProps } from "./types.ts";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user