diff --git a/.gitignore b/.gitignore
index 63e78fe94..9c5b57e15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,5 @@ dump
/cypress/videos
/cypress/screenshots
-.DS_Store
\ No newline at end of file
+.DS_Store
+.excalidraw
\ No newline at end of file
diff --git a/app/components/SubNav.tsx b/app/components/SubNav.tsx
index 7fd1895b8..451b2597a 100644
--- a/app/components/SubNav.tsx
+++ b/app/components/SubNav.tsx
@@ -5,7 +5,11 @@ import type * as React from "react";
import { ArrowUpIcon } from "./icons/ArrowUp";
export function SubNav({ children }: { children: React.ReactNode }) {
- return {children} ;
+ return (
+
+ {children}
+
+ );
}
export function SubNavLink({
diff --git a/app/components/layout/Footer.tsx b/app/components/layout/Footer.tsx
new file mode 100644
index 000000000..c3c33b4e5
--- /dev/null
+++ b/app/components/layout/Footer.tsx
@@ -0,0 +1,92 @@
+import { Link, useLoaderData } from "@remix-run/react";
+import type { RootLoaderData } from "~/root";
+import { discordFullName } from "~/utils/strings";
+import {
+ FAQ_PAGE,
+ SENDOU_INK_DISCORD_URL,
+ SENDOU_INK_GITHUB_URL,
+ SENDOU_INK_PATREON_URL,
+ SENDOU_TWITTER_URL,
+ userPage,
+} from "~/utils/urls";
+import { DiscordIcon } from "../icons/Discord";
+import { GitHubIcon } from "../icons/GitHub";
+import { PatreonIcon } from "../icons/Patreon";
+import { TwitterIcon } from "../icons/Twitter";
+
+// xxx: add some sendouLove
+export function Footer() {
+ const data = useLoaderData();
+
+ return (
+
+
+
+
+
+ Thanks to the patrons for the support
+
+
+ {data.patrons.map((patron) => (
+
+
+ {discordFullName(patron)}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx
index 66d821da5..4607e1925 100644
--- a/app/components/layout/index.tsx
+++ b/app/components/layout/index.tsx
@@ -5,6 +5,7 @@ import { UserItem } from "./UserItem";
import navItems from "./nav-items.json";
import { useLocation } from "@remix-run/react";
import { Image } from "../Image";
+import { Footer } from "./Footer";
export const Layout = React.memo(function Layout({
children,
@@ -19,7 +20,7 @@ export const Layout = React.memo(function Layout({
);
return (
- <>
+
{currentPagesNavItem ? (
@@ -45,6 +46,7 @@ export const Layout = React.memo(function Layout({
setMenuOpen(false)} />
{children}
- >
+
+
);
});
diff --git a/app/db/models/users.server.ts b/app/db/models/users.server.ts
index b3b42dcbc..b4b8bdbca 100644
--- a/app/db/models/users.server.ts
+++ b/app/db/models/users.server.ts
@@ -145,3 +145,25 @@ export function findAllPlusMembers() {
plusTier: NonNullable;
}>;
}
+
+const findAllPatronsStm = sql.prepare(`
+ select
+ "id",
+ "discordId",
+ "discordName",
+ "discordDiscriminator",
+ "patronTier"
+ from "User"
+ where "patronTier" is not null
+ order by "patronTier" desc, "patronSince" asc
+`);
+
+export type FindAllPatrons = Array<
+ Pick<
+ User,
+ "id" | "discordId" | "discordName" | "discordDiscriminator" | "patronTier"
+ >
+>;
+export function findAllPatrons() {
+ return findAllPatronsStm.all() as FindAllPatrons;
+}
diff --git a/app/db/seed.ts b/app/db/seed.ts
index a6544637d..9c89bbf5d 100644
--- a/app/db/seed.ts
+++ b/app/db/seed.ts
@@ -9,6 +9,7 @@ import { sql } from "~/db/sql";
import type { UpsertManyPlusVotesArgs } from "./models/plusVotes.server";
import { ADMIN_DISCORD_ID } from "~/constants";
import shuffle from "just-shuffle";
+import { dateToDatabaseTimestamp } from "~/utils/dates";
const ADMIN_TEST_AVATAR = "fcfd65a3bea598905abb9ca25296816b";
@@ -26,6 +27,7 @@ const basicSeeds = [
badgesToAdmin,
badgesToUsers,
badgeManagers,
+ patrons,
];
export function seed() {
@@ -42,7 +44,7 @@ function wipeDB() {
"PlusVote",
"PlusSuggestion",
"PlusVote",
- "BadgeOwner",
+ "TournamentBadgeOwner",
"BadgeManager",
];
@@ -226,7 +228,7 @@ function badgesToAdmin() {
for (const id of badgesWithDuplicates) {
sql
.prepare(
- `insert into "BadgeOwner" ("badgeId", "userId") values ($id, $userId)`
+ `insert into "TournamentBadgeOwner" ("badgeId", "userId") values ($id, $userId)`
)
.run({ id, userId: 1 });
}
@@ -259,7 +261,7 @@ function badgesToUsers() {
const userToGetABadge = userIds.shift()!;
sql
.prepare(
- `insert into "BadgeOwner" ("badgeId", "userId") values ($id, $userId)`
+ `insert into "TournamentBadgeOwner" ("badgeId", "userId") values ($id, $userId)`
)
.run({ id, userId: userToGetABadge });
@@ -278,3 +280,22 @@ function badgeManagers() {
.run({ id, userId: 2 });
}
}
+
+function patrons() {
+ const userIds = sql
+ .prepare(`select "id" from "User" order by random() limit 50`)
+ .all()
+ .map((u) => u.id);
+
+ for (const id of userIds) {
+ sql
+ .prepare(
+ `update user set "patronTier" = $patronTier, "patronSince" = $patronSince where id = $id`
+ )
+ .run({
+ id,
+ patronSince: dateToDatabaseTimestamp(faker.date.past()),
+ patronTier: faker.helpers.arrayElement([1, 1, 2, 2, 2, 3, 3, 4]),
+ });
+ }
+}
diff --git a/app/root.tsx b/app/root.tsx
index 10605e66d..9b37d72c4 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -20,6 +20,8 @@ import layoutStyles from "~/styles/layout.css";
import resetStyles from "~/styles/reset.css";
import { Catcher } from "./components/Catcher";
import { Layout } from "./components/layout";
+import { db } from "./db";
+import type { FindAllPatrons } from "./db/models/users.server";
import type { UserWithPlusTier } from "./db/types";
import { getUser } from "./modules/auth";
@@ -41,6 +43,7 @@ export const meta: MetaFunction = () => ({
});
export interface RootLoaderData {
+ patrons: FindAllPatrons;
user?: Pick<
UserWithPlusTier,
"id" | "discordId" | "discordAvatar" | "plusTier"
@@ -51,6 +54,7 @@ export const loader: LoaderFunction = async ({ request }) => {
const user = await getUser(request);
return json({
+ patrons: db.users.findAllPatrons(),
user: user
? {
discordAvatar: user.discordAvatar,
diff --git a/app/styles/global.css b/app/styles/global.css
index 3776e2d7e..4f09cce7c 100644
--- a/app/styles/global.css
+++ b/app/styles/global.css
@@ -23,6 +23,7 @@
--theme-vibrant: hsl(255deg 78% 65%);
--theme-transparent: hsl(255deg 66.7% 75% / 40%);
--theme-transparent-vibrant: hsl(255deg 78% 65% / 54%);
+ --theme-semi-transparent-vibrant: hsl(255deg 78% 65% / 75%);
--theme-secondary: hsl(85deg 66.7% 55.3%);
/* background-pattern.svg not using this currently */
@@ -312,6 +313,11 @@ article {
white-space: pre-line;
}
+ol,
+ul {
+ padding: 0;
+}
+
select {
all: unset;
width: 90%;
diff --git a/app/styles/layout.css b/app/styles/layout.css
index 578a46261..f31740247 100644
--- a/app/styles/layout.css
+++ b/app/styles/layout.css
@@ -1,3 +1,9 @@
+.layout__container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
.layout__header {
--item-size: 2.75rem;
@@ -35,6 +41,7 @@
margin: 0 auto;
padding-block-end: var(--s-32);
padding-inline: var(--s-3);
+ width: 100%;
}
.layout__burger {
@@ -182,3 +189,97 @@
flex-direction: column;
gap: var(--s-2);
}
+
+.layout__footer {
+ background-color: var(--bg-darker);
+ margin-block-start: auto;
+ padding: var(--s-2-5);
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-6);
+}
+
+.layout__footer__link-list {
+ display: flex;
+ justify-content: space-between;
+ font-size: var(--fonts-xxs);
+}
+
+.layout__footer__link-list > a {
+ flex: 1 1 0;
+ text-align: center;
+}
+
+.layout__footer__socials {
+ display: flex;
+ gap: var(--s-2);
+ justify-content: center;
+}
+
+.layout__footer__social-link {
+ background-color: var(--theme-transparent-vibrant);
+ border-radius: var(--rounded);
+ height: 16rem;
+ flex: 1 1 0;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: var(--s-4);
+ font-size: var(--fonts-lg);
+ cursor: pointer;
+ max-width: 10rem;
+}
+
+.layout__footer__social-link:hover {
+ background-color: var(--theme-semi-transparent-vibrant);
+}
+
+.layout__footer__social-link:hover > .layout__footer__social-icon {
+ transform: translateY(-0.3rem);
+}
+
+/* .layout__footer__social-link:hover > .layout__footer__social-icon.github {
+ fill: #333;
+}
+
+.layout__footer__social-link:hover > .layout__footer__social-icon.twitter {
+ fill: #1da1f2;
+}
+
+.layout__footer__social-link:hover > .layout__footer__social-icon.discord {
+ fill: #5865f2;
+}
+
+.layout__footer__social-link:hover > .layout__footer__social-icon.patreon {
+ fill: #f96854;
+} */
+
+.layout__footer__social-icon {
+ height: 2.25rem;
+ transition: transform 0.25s ease-in-out;
+}
+
+.layout__footer__social-header {
+ text-align: center;
+}
+
+.layout__footer__social-header > p {
+ font-size: var(--fonts-xxs);
+}
+
+.layout__footer__patron-title {
+ text-align: center;
+ font-weight: var(--semi-bold);
+ font-size: var(--fonts-sm);
+}
+
+.layout__footer__patron-list {
+ display: flex;
+ list-style: none;
+ flex-wrap: wrap;
+ font-size: var(--fonts-xs);
+ gap: var(--s-1);
+ justify-content: center;
+ margin-block-start: var(--s-2);
+}
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index 9eaf8ea09..b4f5fa730 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -1,6 +1,7 @@
import type { Badge } from "~/db/types";
export const SENDOU_INK_DISCORD_URL = "https://discord.gg/sendou";
+export const SENDOU_TWITTER_URL = "https://twitter.com/sendouc";
export const SENDOU_INK_TWITTER_URL = "https://twitter.com/sendouink";
export const SENDOU_INK_PATREON_URL = "https://patreon.com/sendou";
export const SENDOU_INK_GITHUB_URL = "https://github.com/Sendouc/sendou.ink";