mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-28 14:18:04 -05:00
Adjust plus routes
This commit is contained in:
@@ -2,11 +2,11 @@ import { Outlet } from "@remix-run/react";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubNav, SubNavLink } from "~/components/SubNav";
|
||||
|
||||
export default function UserPageLayout() {
|
||||
export default function PlusPageLayout() {
|
||||
return (
|
||||
<>
|
||||
<SubNav>
|
||||
<SubNavLink to="" data-cy="profile-page-link">
|
||||
<SubNavLink to="suggestions" data-cy="profile-page-link">
|
||||
Profile
|
||||
</SubNavLink>
|
||||
<SubNavLink to="voting" data-cy="edit-page-link">
|
||||
|
||||
@@ -1,174 +1,5 @@
|
||||
import type {
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
MetaFunction,
|
||||
} from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Outlet, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Catcher } from "~/components/Catcher";
|
||||
import { upcomingVoting } from "~/core/plus";
|
||||
import { db } from "~/db";
|
||||
import type * as plusSuggestions from "~/db/models/plusSuggestions.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import styles from "~/styles/plus.css";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { makeTitle, requireUser } from "~/utils/remix";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { redirect } from "@remix-run/node";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
export const loader = () => {
|
||||
return redirect("/plus/suggestions");
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
title: makeTitle("Plus Server suggestions"),
|
||||
description: "This month's suggestions for +1, +2 and +3.",
|
||||
};
|
||||
};
|
||||
|
||||
interface PlusLoaderData {
|
||||
suggestions?: plusSuggestions.FindResult;
|
||||
suggestedForTiers: number[];
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
return json<PlusLoaderData>({
|
||||
suggestions: db.plusSuggestions.find({
|
||||
...upcomingVoting(new Date()),
|
||||
plusTier: user.plusTier,
|
||||
}),
|
||||
suggestedForTiers: db.plusSuggestions.tiersSuggestedFor({
|
||||
...upcomingVoting(new Date()),
|
||||
userId: user.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default function PlusPage() {
|
||||
const data = useLoaderData<PlusLoaderData>();
|
||||
const [tierVisible, setTierVisible] = React.useState(
|
||||
data.suggestions?.[0].tier ?? 0
|
||||
);
|
||||
|
||||
if (!data.suggestions) {
|
||||
return <SuggestedForInfo />;
|
||||
}
|
||||
|
||||
const visibleSuggestions = data.suggestions.find(
|
||||
({ tier }) => tier === tierVisible
|
||||
);
|
||||
invariant(visibleSuggestions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<div className="plus__container">
|
||||
<SuggestedForInfo />
|
||||
<div className="stack md">
|
||||
<div className="plus__radios">
|
||||
{data.suggestions.map(({ tier, users }) => {
|
||||
const id = String(tier);
|
||||
return (
|
||||
<div key={id} className="plus__radio-container">
|
||||
<label htmlFor={id} className="plus__radio-label">
|
||||
+{tier}{" "}
|
||||
<span className="plus__users-count">({users.length})</span>
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
name="tier"
|
||||
type="radio"
|
||||
checked={tierVisible === tier}
|
||||
onChange={() => setTierVisible(tier)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="stack lg">
|
||||
{visibleSuggestions.users.map((u) => (
|
||||
<SuggestedUser key={`${u.info.id}-${tierVisible}`} user={u} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SuggestedForInfo() {
|
||||
const data = useLoaderData<PlusLoaderData>();
|
||||
const user = useUser();
|
||||
|
||||
// no need to show anything if they can't be suggested anyway...
|
||||
if (user?.plusTier === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.suggestedForTiers.length === 0) {
|
||||
return (
|
||||
<div className="plus__suggested-info-text">
|
||||
You are not suggested yet this month.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="plus__suggested-info-text">
|
||||
You are suggested for{" "}
|
||||
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
|
||||
month.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SuggestedUser({
|
||||
user,
|
||||
}: {
|
||||
user: Unpacked<Unpacked<NonNullable<PlusLoaderData["suggestions"]>>["users"]>;
|
||||
}) {
|
||||
return (
|
||||
<div className="stack md">
|
||||
<div className="plus__suggested-user-info">
|
||||
<Avatar
|
||||
discordAvatar={user.info.discordAvatar}
|
||||
discordId={user.info.discordId}
|
||||
size="md"
|
||||
/>
|
||||
<h2>{user.info.discordName}</h2>
|
||||
<Button className="plus__comment-button" tiny variant="outlined">
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
<details>
|
||||
<summary className="plus__view-comments-action">
|
||||
Comments ({user.suggestions.length})
|
||||
</summary>
|
||||
<div className="stack sm mt-2">
|
||||
{user.suggestions.map((s) => (
|
||||
<fieldset key={s.author.id}>
|
||||
<legend>{discordFullName(s.author)}</legend>
|
||||
{s.text}
|
||||
<span className="plus__comment-time">
|
||||
{" "}
|
||||
―{" "}
|
||||
<time>
|
||||
{databaseTimestampToDate(s.createdAt).toLocaleString()}
|
||||
</time>
|
||||
</span>
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CatchBoundary = Catcher;
|
||||
|
||||
174
app/routes/plus/suggestions.tsx
Normal file
174
app/routes/plus/suggestions.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import type {
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
MetaFunction,
|
||||
} from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Outlet, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Catcher } from "~/components/Catcher";
|
||||
import { upcomingVoting } from "~/core/plus";
|
||||
import { db } from "~/db";
|
||||
import type * as plusSuggestions from "~/db/models/plusSuggestions.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import styles from "~/styles/plus.css";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { makeTitle, requireUser } from "~/utils/remix";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
title: makeTitle("Plus Server suggestions"),
|
||||
description: "This month's suggestions for +1, +2 and +3.",
|
||||
};
|
||||
};
|
||||
|
||||
interface PlusLoaderData {
|
||||
suggestions?: plusSuggestions.FindResult;
|
||||
suggestedForTiers: number[];
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
return json<PlusLoaderData>({
|
||||
suggestions: db.plusSuggestions.find({
|
||||
...upcomingVoting(new Date()),
|
||||
plusTier: user.plusTier,
|
||||
}),
|
||||
suggestedForTiers: db.plusSuggestions.tiersSuggestedFor({
|
||||
...upcomingVoting(new Date()),
|
||||
userId: user.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default function PlusPage() {
|
||||
const data = useLoaderData<PlusLoaderData>();
|
||||
const [tierVisible, setTierVisible] = React.useState(
|
||||
data.suggestions?.[0].tier ?? 0
|
||||
);
|
||||
|
||||
if (!data.suggestions) {
|
||||
return <SuggestedForInfo />;
|
||||
}
|
||||
|
||||
const visibleSuggestions = data.suggestions.find(
|
||||
({ tier }) => tier === tierVisible
|
||||
);
|
||||
invariant(visibleSuggestions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<div className="plus__container">
|
||||
<SuggestedForInfo />
|
||||
<div className="stack md">
|
||||
<div className="plus__radios">
|
||||
{data.suggestions.map(({ tier, users }) => {
|
||||
const id = String(tier);
|
||||
return (
|
||||
<div key={id} className="plus__radio-container">
|
||||
<label htmlFor={id} className="plus__radio-label">
|
||||
+{tier}{" "}
|
||||
<span className="plus__users-count">({users.length})</span>
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
name="tier"
|
||||
type="radio"
|
||||
checked={tierVisible === tier}
|
||||
onChange={() => setTierVisible(tier)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="stack lg">
|
||||
{visibleSuggestions.users.map((u) => (
|
||||
<SuggestedUser key={`${u.info.id}-${tierVisible}`} user={u} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SuggestedForInfo() {
|
||||
const data = useLoaderData<PlusLoaderData>();
|
||||
const user = useUser();
|
||||
|
||||
// no need to show anything if they can't be suggested anyway...
|
||||
if (user?.plusTier === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.suggestedForTiers.length === 0) {
|
||||
return (
|
||||
<div className="plus__suggested-info-text">
|
||||
You are not suggested yet this month.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="plus__suggested-info-text">
|
||||
You are suggested for{" "}
|
||||
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
|
||||
month.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SuggestedUser({
|
||||
user,
|
||||
}: {
|
||||
user: Unpacked<Unpacked<NonNullable<PlusLoaderData["suggestions"]>>["users"]>;
|
||||
}) {
|
||||
return (
|
||||
<div className="stack md">
|
||||
<div className="plus__suggested-user-info">
|
||||
<Avatar
|
||||
discordAvatar={user.info.discordAvatar}
|
||||
discordId={user.info.discordId}
|
||||
size="md"
|
||||
/>
|
||||
<h2>{user.info.discordName}</h2>
|
||||
<Button className="plus__comment-button" tiny variant="outlined">
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
<details>
|
||||
<summary className="plus__view-comments-action">
|
||||
Comments ({user.suggestions.length})
|
||||
</summary>
|
||||
<div className="stack sm mt-2">
|
||||
{user.suggestions.map((s) => (
|
||||
<fieldset key={s.author.id}>
|
||||
<legend>{discordFullName(s.author)}</legend>
|
||||
{s.text}
|
||||
<span className="plus__comment-time">
|
||||
{" "}
|
||||
―{" "}
|
||||
<time>
|
||||
{databaseTimestampToDate(s.createdAt).toLocaleString()}
|
||||
</time>
|
||||
</span>
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CatchBoundary = Catcher;
|
||||
@@ -4,7 +4,7 @@ import { Dialog } from "~/components/Dialog";
|
||||
export default function PlusCommentModalPage() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Dialog isOpen close={() => navigate("/plus")}>
|
||||
<Dialog isOpen close={() => navigate("/plus/suggestions")}>
|
||||
hello world
|
||||
</Dialog>
|
||||
);
|
||||
Reference in New Issue
Block a user