summaryrefslogtreecommitdiff
path: root/garden/src
diff options
context:
space:
mode:
Diffstat (limited to 'garden/src')
-rw-r--r--garden/src/components/Layout.tsx70
-rw-r--r--garden/src/pages/home.tsx3
-rw-r--r--garden/src/store/auth.ts9
-rw-r--r--garden/src/store/stats.ts14
-rw-r--r--garden/src/styles/layout.css215
-rw-r--r--garden/src/types/stats.ts13
6 files changed, 262 insertions, 62 deletions
diff --git a/garden/src/components/Layout.tsx b/garden/src/components/Layout.tsx
index e5d5bf8..4ff2e8e 100644
--- a/garden/src/components/Layout.tsx
+++ b/garden/src/components/Layout.tsx
@@ -1,8 +1,9 @@
-import { type JSX, Show, onMount } from "solid-js";
+import { type JSX, Show, For, onMount, onCleanup, createEffect } from "solid-js";
import { A } from "@solidjs/router";
import Sidebar from "./Sidebar";
import NavSection from "./NavSection";
import { auth } from "../store/auth";
+import { stats } from "../store/stats";
import { UserRole } from "../types/roles";
interface LayoutProps {
@@ -10,7 +11,22 @@ interface LayoutProps {
}
export default function Layout(props: LayoutProps) {
- onMount(() => auth.initialize());
+ let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
+
+ onMount(() => {
+ auth.initialize();
+ stats.load();
+ });
+
+ createEffect(() => {
+ clearInterval(heartbeatInterval);
+ if (auth.user()) {
+ auth.heartbeat();
+ heartbeatInterval = setInterval(() => auth.heartbeat(), 2 * 60 * 1000);
+ }
+ });
+
+ onCleanup(() => clearInterval(heartbeatInterval));
return (
<>
@@ -23,12 +39,12 @@ export default function Layout(props: LayoutProps) {
<A href="/" class="top-nav-link" data-accent="cyan" activeClass="active" end>Home</A>
<A href="/districts" class="top-nav-link" data-accent="green" activeClass="active">Districts</A>
<A href="/forums" class="top-nav-link" data-accent="pink" activeClass="active">Forums</A>
- <A href="/chat" class="top-nav-link" data-accent="yellow" activeClass="active">Chat</A>
+ <A href="/tavern" class="top-nav-link" data-accent="yellow" activeClass="active">Tavern</A>
<A href="/bazaar" class="top-nav-link" data-accent="purple" activeClass="active">Bazaar</A>
</nav>
<div class="search-bar">
- <input type="text" placeholder="Search members, posts, districts..." />
+ <input type="text" placeholder="Search citizens, posts, districts..." />
</div>
<div class="site-main">
@@ -43,11 +59,9 @@ export default function Layout(props: LayoutProps) {
}>
{((user) => (
<>
- <li><A href="/account">Account</A></li>
- <li><A href={`/u/${user.username}`}>My Page</A></li>
+ <li><A href={`/u/${user.username}`}>My Domain</A></li>
<li><A href="/letters">Letters</A></li>
- <li><A href="/notifications">Notifications</A></li>
- <li><A href="/friends">Friends</A></li>
+ <li><A href="/account">Account</A></li>
<li><A href="/account/settings">Settings</A></li>
<li><button type="button" class="sidebar-logout" onClick={() => auth.logout()}>Log Out</button></li>
</>
@@ -57,13 +71,17 @@ export default function Layout(props: LayoutProps) {
</NavSection>
<NavSection title="Community" accent="cyan">
<ul>
- <li><A href="/members">Members</A></li>
- <li><A href="/online">Who's Online</A></li>
- <li><A href="/random">Random Member</A></li>
+ <li><A href="/citizens">Citizens</A></li>
<li><A href="/clubs">Clubs</A></li>
<li><A href="/interests">Interests</A></li>
</ul>
</NavSection>
+ <NavSection title="Explore" accent="yellow">
+ <ul>
+ <li><A href="/online">Who's Online</A></li>
+ <li><A href="/random">Random Domain</A></li>
+ </ul>
+ </NavSection>
<NavSection title="Services" accent="green">
<ul>
<li><A href="/caravan">Caravan</A></li>
@@ -96,18 +114,34 @@ export default function Layout(props: LayoutProps) {
<Sidebar>
<NavSection title="Statistics" accent="yellow">
<ul>
- <li>Members: —</li>
- <li>Online: —</li>
+ <li>Citizens: {stats.data()?.citizens ?? "—"}</li>
+ <li>Online: {stats.data()?.online ?? "—"}</li>
<li>Posts Today: —</li>
- <li>Newest: —</li>
</ul>
</NavSection>
- <NavSection title="New Members" accent="cyan">
+ <NavSection title="New Citizens" accent="cyan">
+ <ul>
+ <Show when={stats.data()?.newest_citizens?.length} fallback={
+ <li class="placeholder">Be the first to join!</li>
+ }>
+ <For each={stats.data()?.newest_citizens}>
+ {(citizen) => <li><A href={`/u/${citizen.username}`}>{citizen.display_name}</A></li>}
+ </For>
+ </Show>
+ </ul>
+ </NavSection>
+ <NavSection title="Who's Online" accent="green">
<ul>
- <li class="placeholder">Be the first to join!</li>
+ <Show when={stats.data()?.online_citizens?.length} fallback={
+ <li class="placeholder">No one online.</li>
+ }>
+ <For each={stats.data()?.online_citizens}>
+ {(citizen) => <li><A href={`/u/${citizen.username}`}>{citizen.display_name}</A></li>}
+ </For>
+ </Show>
</ul>
</NavSection>
- <NavSection title="Birthdays" accent="green">
+ <NavSection title="Birthdays" accent="pink">
<ul>
<li class="placeholder">No birthdays today.</li>
</ul>
@@ -133,7 +167,7 @@ export default function Layout(props: LayoutProps) {
<A href="/terms">Terms</A>
<A href="/contact">Contact</A>
</nav>
- <p>&copy; {new Date().getFullYear()} Pagoda. Brought to you by <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">shi.foo</a>.</p>
+ <p>&copy; {new Date().getFullYear()} Pagoda. Brought to you by <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">shi.foo</a>. Powered by <a href="https://nekoweb.org" target="_blank" rel="noopener noreferrer">Nekoweb</a>.</p>
</footer>
</>
);
diff --git a/garden/src/pages/home.tsx b/garden/src/pages/home.tsx
index 96a2fa2..3e07a84 100644
--- a/garden/src/pages/home.tsx
+++ b/garden/src/pages/home.tsx
@@ -1,8 +1,7 @@
export default function Home() {
return (
<section>
- <p>Welcome to <em>Pagoda</em>! A community for the small web, powered by <a href="https://nekoweb.org" target="_blank" rel="noopener noreferrer">nekoweb</a>. Explore districts, join forums, chat with fellow web enthusiasts, and customize your own page.</p>
- <p>Pagoda is currently under construction. Check back soon.</p>
+ <p></p>
</section>
);
} \ No newline at end of file
diff --git a/garden/src/store/auth.ts b/garden/src/store/auth.ts
index 7dca3c2..915e4de 100644
--- a/garden/src/store/auth.ts
+++ b/garden/src/store/auth.ts
@@ -88,6 +88,13 @@ async function reactivate(email: string): Promise<string | null> {
return (response.data as ErrorResponse).error;
}
+async function heartbeat() {
+ const stored = token();
+ if (stored) {
+ await api("/auth/heartbeat", { method: "POST", token: stored });
+ }
+}
+
async function logout() {
const stored = token();
if (stored) {
@@ -98,4 +105,4 @@ async function logout() {
setUser(null);
}
-export const auth = { user, token, loading, initialize, login, register, verify, reactivate, logout }; \ No newline at end of file
+export const auth = { user, token, loading, initialize, login, register, verify, reactivate, heartbeat, logout }; \ No newline at end of file
diff --git a/garden/src/store/stats.ts b/garden/src/store/stats.ts
new file mode 100644
index 0000000..e13164f
--- /dev/null
+++ b/garden/src/store/stats.ts
@@ -0,0 +1,14 @@
+import { createSignal } from "solid-js";
+import { api } from "../api";
+import type { Stats } from "../types/stats";
+
+const [data, setData] = createSignal<Stats | null>(null);
+
+async function load() {
+ const response = await api<Stats>("/stats/");
+ if (response.ok) {
+ setData(response.data);
+ }
+}
+
+export const stats = { data, load }; \ No newline at end of file
diff --git a/garden/src/styles/layout.css b/garden/src/styles/layout.css
index 19c0235..4e55e9b 100644
--- a/garden/src/styles/layout.css
+++ b/garden/src/styles/layout.css
@@ -25,7 +25,7 @@ a:hover {
}
.site-header {
- padding: 16px 0 8px;
+ padding: 0px 0 16px 0;
text-align: center;
position: relative;
}
@@ -93,17 +93,65 @@ a:hover {
border-right: none;
}
-.top-nav-link[data-accent="cyan"]:hover { border-top-color: var(--color-cyan); color: var(--color-cyan); background: color-mix(in srgb, var(--color-cyan) 6%, transparent); }
-.top-nav-link[data-accent="green"]:hover { border-top-color: var(--color-green); color: var(--color-green); background: color-mix(in srgb, var(--color-green) 6%, transparent); }
-.top-nav-link[data-accent="pink"]:hover { border-top-color: var(--color-pink); color: var(--color-pink); background: color-mix(in srgb, var(--color-pink) 6%, transparent); }
-.top-nav-link[data-accent="yellow"]:hover { border-top-color: var(--color-yellow); color: var(--color-yellow); background: color-mix(in srgb, var(--color-yellow) 6%, transparent); }
-.top-nav-link[data-accent="purple"]:hover { border-top-color: var(--color-purple); color: var(--color-purple); background: color-mix(in srgb, var(--color-purple) 6%, transparent); }
+.top-nav-link[data-accent="cyan"]:hover {
+ border-top-color: var(--color-cyan);
+ color: var(--color-cyan);
+ background: color-mix(in srgb, var(--color-cyan) 6%, transparent);
+}
+
+.top-nav-link[data-accent="green"]:hover {
+ border-top-color: var(--color-green);
+ color: var(--color-green);
+ background: color-mix(in srgb, var(--color-green) 6%, transparent);
+}
+
+.top-nav-link[data-accent="pink"]:hover {
+ border-top-color: var(--color-pink);
+ color: var(--color-pink);
+ background: color-mix(in srgb, var(--color-pink) 6%, transparent);
+}
+
+.top-nav-link[data-accent="yellow"]:hover {
+ border-top-color: var(--color-yellow);
+ color: var(--color-yellow);
+ background: color-mix(in srgb, var(--color-yellow) 6%, transparent);
+}
+
+.top-nav-link[data-accent="purple"]:hover {
+ border-top-color: var(--color-purple);
+ color: var(--color-purple);
+ background: color-mix(in srgb, var(--color-purple) 6%, transparent);
+}
-.top-nav-link[data-accent="cyan"].active { border-top-color: var(--color-cyan); color: var(--color-cyan); background: color-mix(in srgb, var(--color-cyan) 10%, transparent); }
-.top-nav-link[data-accent="green"].active { border-top-color: var(--color-green); color: var(--color-green); background: color-mix(in srgb, var(--color-green) 10%, transparent); }
-.top-nav-link[data-accent="pink"].active { border-top-color: var(--color-pink); color: var(--color-pink); background: color-mix(in srgb, var(--color-pink) 10%, transparent); }
-.top-nav-link[data-accent="yellow"].active { border-top-color: var(--color-yellow); color: var(--color-yellow); background: color-mix(in srgb, var(--color-yellow) 10%, transparent); }
-.top-nav-link[data-accent="purple"].active { border-top-color: var(--color-purple); color: var(--color-purple); background: color-mix(in srgb, var(--color-purple) 10%, transparent); }
+.top-nav-link[data-accent="cyan"].active {
+ border-top-color: var(--color-cyan);
+ color: var(--color-cyan);
+ background: color-mix(in srgb, var(--color-cyan) 10%, transparent);
+}
+
+.top-nav-link[data-accent="green"].active {
+ border-top-color: var(--color-green);
+ color: var(--color-green);
+ background: color-mix(in srgb, var(--color-green) 10%, transparent);
+}
+
+.top-nav-link[data-accent="pink"].active {
+ border-top-color: var(--color-pink);
+ color: var(--color-pink);
+ background: color-mix(in srgb, var(--color-pink) 10%, transparent);
+}
+
+.top-nav-link[data-accent="yellow"].active {
+ border-top-color: var(--color-yellow);
+ color: var(--color-yellow);
+ background: color-mix(in srgb, var(--color-yellow) 10%, transparent);
+}
+
+.top-nav-link[data-accent="purple"].active {
+ border-top-color: var(--color-purple);
+ color: var(--color-purple);
+ background: color-mix(in srgb, var(--color-purple) 10%, transparent);
+}
.search-bar {
width: var(--width-container);
@@ -199,12 +247,29 @@ a:hover {
border: 1px solid var(--color-border);
}
-.nav-section[data-accent="cyan"] { border-top: 2px solid var(--color-cyan); }
-.nav-section[data-accent="green"] { border-top: 2px solid var(--color-green); }
-.nav-section[data-accent="pink"] { border-top: 2px solid var(--color-pink); }
-.nav-section[data-accent="yellow"] { border-top: 2px solid var(--color-yellow); }
-.nav-section[data-accent="purple"] { border-top: 2px solid var(--color-purple); }
-.nav-section[data-accent="red"] { border-top: 2px solid var(--color-red); }
+.nav-section[data-accent="cyan"] {
+ border-top: 2px solid var(--color-cyan);
+}
+
+.nav-section[data-accent="green"] {
+ border-top: 2px solid var(--color-green);
+}
+
+.nav-section[data-accent="pink"] {
+ border-top: 2px solid var(--color-pink);
+}
+
+.nav-section[data-accent="yellow"] {
+ border-top: 2px solid var(--color-yellow);
+}
+
+.nav-section[data-accent="purple"] {
+ border-top: 2px solid var(--color-purple);
+}
+
+.nav-section[data-accent="red"] {
+ border-top: 2px solid var(--color-red);
+}
.nav-section-header {
background: var(--color-panel-header);
@@ -218,12 +283,29 @@ a:hover {
color: var(--color-text-bright);
}
-.nav-section[data-accent="cyan"] .nav-section-header { color: var(--color-cyan); }
-.nav-section[data-accent="green"] .nav-section-header { color: var(--color-green); }
-.nav-section[data-accent="pink"] .nav-section-header { color: var(--color-pink); }
-.nav-section[data-accent="yellow"] .nav-section-header { color: var(--color-yellow); }
-.nav-section[data-accent="purple"] .nav-section-header { color: var(--color-purple); }
-.nav-section[data-accent="red"] .nav-section-header { color: var(--color-red); }
+.nav-section[data-accent="cyan"] .nav-section-header {
+ color: var(--color-cyan);
+}
+
+.nav-section[data-accent="green"] .nav-section-header {
+ color: var(--color-green);
+}
+
+.nav-section[data-accent="pink"] .nav-section-header {
+ color: var(--color-pink);
+}
+
+.nav-section[data-accent="yellow"] .nav-section-header {
+ color: var(--color-yellow);
+}
+
+.nav-section[data-accent="purple"] .nav-section-header {
+ color: var(--color-purple);
+}
+
+.nav-section[data-accent="red"] .nav-section-header {
+ color: var(--color-red);
+}
.nav-section-body ul {
list-style: none;
@@ -243,35 +325,86 @@ a:hover {
color: var(--color-text-muted);
}
-.nav-section[data-accent="cyan"] .nav-section-body li::before { color: var(--color-cyan); }
-.nav-section[data-accent="green"] .nav-section-body li::before { color: var(--color-green); }
-.nav-section[data-accent="pink"] .nav-section-body li::before { color: var(--color-pink); }
-.nav-section[data-accent="yellow"] .nav-section-body li::before { color: var(--color-yellow); }
-.nav-section[data-accent="purple"] .nav-section-body li::before { color: var(--color-purple); }
-.nav-section[data-accent="red"] .nav-section-body li::before { color: var(--color-red); }
+.nav-section[data-accent="cyan"] .nav-section-body li::before {
+ color: var(--color-cyan);
+}
+
+.nav-section[data-accent="green"] .nav-section-body li::before {
+ color: var(--color-green);
+}
+
+.nav-section[data-accent="pink"] .nav-section-body li::before {
+ color: var(--color-pink);
+}
+
+.nav-section[data-accent="yellow"] .nav-section-body li::before {
+ color: var(--color-yellow);
+}
+
+.nav-section[data-accent="purple"] .nav-section-body li::before {
+ color: var(--color-purple);
+}
+
+.nav-section[data-accent="red"] .nav-section-body li::before {
+ color: var(--color-red);
+}
.nav-section-body li a {
color: var(--color-link);
text-decoration: none;
}
-.nav-section[data-accent="cyan"] .nav-section-body li a:hover { color: var(--color-cyan); }
-.nav-section[data-accent="green"] .nav-section-body li a:hover { color: var(--color-green); }
-.nav-section[data-accent="pink"] .nav-section-body li a:hover { color: var(--color-pink); }
-.nav-section[data-accent="yellow"] .nav-section-body li a:hover { color: var(--color-yellow); }
-.nav-section[data-accent="purple"] .nav-section-body li a:hover { color: var(--color-purple); }
-.nav-section[data-accent="red"] .nav-section-body li a:hover { color: var(--color-red); }
+.nav-section[data-accent="cyan"] .nav-section-body li a:hover {
+ color: var(--color-cyan);
+}
+
+.nav-section[data-accent="green"] .nav-section-body li a:hover {
+ color: var(--color-green);
+}
+
+.nav-section[data-accent="pink"] .nav-section-body li a:hover {
+ color: var(--color-pink);
+}
+
+.nav-section[data-accent="yellow"] .nav-section-body li a:hover {
+ color: var(--color-yellow);
+}
+
+.nav-section[data-accent="purple"] .nav-section-body li a:hover {
+ color: var(--color-purple);
+}
+
+.nav-section[data-accent="red"] .nav-section-body li a:hover {
+ color: var(--color-red);
+}
.nav-section-body li a.active {
font-weight: 600;
}
-.nav-section[data-accent="cyan"] .nav-section-body li a.active { color: var(--color-cyan); }
-.nav-section[data-accent="green"] .nav-section-body li a.active { color: var(--color-green); }
-.nav-section[data-accent="pink"] .nav-section-body li a.active { color: var(--color-pink); }
-.nav-section[data-accent="yellow"] .nav-section-body li a.active { color: var(--color-yellow); }
-.nav-section[data-accent="purple"] .nav-section-body li a.active { color: var(--color-purple); }
-.nav-section[data-accent="red"] .nav-section-body li a.active { color: var(--color-red); }
+.nav-section[data-accent="cyan"] .nav-section-body li a.active {
+ color: var(--color-cyan);
+}
+
+.nav-section[data-accent="green"] .nav-section-body li a.active {
+ color: var(--color-green);
+}
+
+.nav-section[data-accent="pink"] .nav-section-body li a.active {
+ color: var(--color-pink);
+}
+
+.nav-section[data-accent="yellow"] .nav-section-body li a.active {
+ color: var(--color-yellow);
+}
+
+.nav-section[data-accent="purple"] .nav-section-body li a.active {
+ color: var(--color-purple);
+}
+
+.nav-section[data-accent="red"] .nav-section-body li a.active {
+ color: var(--color-red);
+}
.nav-section-body li.placeholder {
color: var(--color-text-muted);
diff --git a/garden/src/types/stats.ts b/garden/src/types/stats.ts
new file mode 100644
index 0000000..9053136
--- /dev/null
+++ b/garden/src/types/stats.ts
@@ -0,0 +1,13 @@
+export interface CitizenSummary {
+ id: number;
+ username: string;
+ display_name: string;
+ avatar_url: string;
+}
+
+export interface Stats {
+ citizens: number;
+ online: number;
+ newest_citizens: CitizenSummary[];
+ online_citizens: CitizenSummary[];
+} \ No newline at end of file