Initial commit
This commit is contained in:
20
src/api.ts
Normal file
20
src/api.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import axios from "axios";
|
||||
|
||||
type UserInfo = {
|
||||
name: string;
|
||||
patreon: boolean;
|
||||
timezone: string | null;
|
||||
};
|
||||
|
||||
export type GuildInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function fetchUserInfo(): Promise<UserInfo> {
|
||||
return axios.get("/api/user").then((resp) => resp.data) as Promise<UserInfo>;
|
||||
}
|
||||
|
||||
export function fetchUserGuilds(): Promise<GuildInfo[]> {
|
||||
return axios.get("/api/user/guilds").then((resp) => resp.data) as Promise<GuildInfo[]>;
|
||||
}
|
32
src/components/App/index.tsx
Normal file
32
src/components/App/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Sidebar } from "../Sidebar";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { Welcome } from "../Welcome";
|
||||
import { GuildReminders } from "../GuildReminders";
|
||||
|
||||
export function App() {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<>
|
||||
<Router base={"/dashboard"}>
|
||||
<div class="columns is-gapless dashboard-frame">
|
||||
<Sidebar></Sidebar>
|
||||
<div class="column is-main-content">
|
||||
<Switch>
|
||||
<Route
|
||||
path={"/:guild/reminders"}
|
||||
component={GuildReminders}
|
||||
></Route>
|
||||
<Route>
|
||||
<Welcome></Welcome>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
7
src/components/GuildReminders/index.tsx
Normal file
7
src/components/GuildReminders/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { useParams } from "wouter";
|
||||
|
||||
export const GuildReminders = () => {
|
||||
const params = useParams();
|
||||
|
||||
return <>{params.guild}</>;
|
||||
};
|
11
src/components/Sidebar/Brand.tsx
Normal file
11
src/components/Sidebar/Brand.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export const Brand = () => (
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/static/img/logo_nobg.webp"
|
||||
alt="Reminder bot logo"
|
||||
width="52px"
|
||||
height="52px"
|
||||
class="dashboard-brand"
|
||||
></img>
|
||||
</div>
|
||||
);
|
5
src/components/Sidebar/DesktopSidebar.tsx
Normal file
5
src/components/Sidebar/DesktopSidebar.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export const DesktopSidebar = ({ children }) => {
|
||||
return (
|
||||
<div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch">{children}</div>
|
||||
);
|
||||
};
|
28
src/components/Sidebar/GuildEntry.tsx
Normal file
28
src/components/Sidebar/GuildEntry.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { GuildInfo } from "../../api";
|
||||
import { Link, useLocation } from "wouter";
|
||||
|
||||
type Props = {
|
||||
guild: GuildInfo;
|
||||
};
|
||||
|
||||
export const GuildEntry = ({ guild }: Props) => {
|
||||
const [loc] = useLocation();
|
||||
const currentId = loc.match(/^\/(?<id>\d+)/);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
class={guild.id === currentId.groups.id ? "is-active switch-pane" : "switch-pane"}
|
||||
data-pane="guild"
|
||||
data-guild={guild.id}
|
||||
data-name={guild.name}
|
||||
href={`/${guild.id}/reminders`}
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="fas fa-map-pin"></i>
|
||||
</span>{" "}
|
||||
<span class="guild-name">{guild.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
7
src/components/Sidebar/MobileSidebar.tsx
Normal file
7
src/components/Sidebar/MobileSidebar.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export const MobileSidebar = ({ children }) => {
|
||||
return (
|
||||
<div class="dashboard-sidebar mobile-sidebar is-hidden-desktop" id="mobileSidebar">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
11
src/components/Sidebar/Wave.tsx
Normal file
11
src/components/Sidebar/Wave.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export const Wave = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160">
|
||||
<g transform="scale(1, 0.5)">
|
||||
<path
|
||||
fill="#8fb677"
|
||||
fill-opacity="1"
|
||||
d="M0,192L60,170.7C120,149,240,107,360,96C480,85,600,107,720,138.7C840,171,960,213,1080,197.3C1200,181,1320,107,1380,69.3L1440,32L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
80
src/components/Sidebar/index.tsx
Normal file
80
src/components/Sidebar/index.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { useQuery } from "react-query";
|
||||
import { DesktopSidebar } from "./DesktopSidebar";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
import { Brand } from "./Brand";
|
||||
import { Wave } from "./Wave";
|
||||
import { GuildEntry } from "./GuildEntry";
|
||||
import { fetchUserGuilds, GuildInfo } from "../../api";
|
||||
import { QueryKeys } from "../../consts";
|
||||
|
||||
type ContentProps = {
|
||||
guilds: GuildInfo[];
|
||||
};
|
||||
|
||||
const SidebarContent = ({ guilds }: ContentProps) => {
|
||||
const guildEntries = guilds.map((guild) => <GuildEntry guild={guild}></GuildEntry>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<a href="/">
|
||||
<Brand></Brand>
|
||||
</a>
|
||||
<Wave></Wave>
|
||||
<aside class="menu">
|
||||
<p class="menu-label">Servers</p>
|
||||
<ul class="menu-list guildList">{guildEntries}</ul>
|
||||
<div class="aside-footer">
|
||||
<p class="menu-label">Options</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a class="show-modal" data-modal="dataManagerModal">
|
||||
<span class="icon">
|
||||
<i class="fas fa-exchange"></i>
|
||||
</span>{" "}
|
||||
Import/Export
|
||||
</a>
|
||||
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||
<span class="icon">
|
||||
<i class="fas fa-map-marked"></i>
|
||||
</span>{" "}
|
||||
Timezone
|
||||
</a>
|
||||
<a href="/login/discord/logout">
|
||||
<span class="icon">
|
||||
<i class="fas fa-sign-out"></i>
|
||||
</span>{" "}
|
||||
Log out
|
||||
</a>
|
||||
<a href="https://discord.jellywx.com" class="feedback">
|
||||
<span class="icon">
|
||||
<i class="fab fa-discord"></i>
|
||||
</span>{" "}
|
||||
Give feedback
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar = () => {
|
||||
const { status, data } = useQuery({
|
||||
queryKey: [QueryKeys.USER_GUILDS],
|
||||
queryFn: fetchUserGuilds,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
let content = <SidebarContent guilds={[]}></SidebarContent>;
|
||||
if (status === "success") {
|
||||
content = <SidebarContent guilds={data}></SidebarContent>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DesktopSidebar>{content}</DesktopSidebar>
|
||||
<MobileSidebar>{content}</MobileSidebar>
|
||||
</>
|
||||
);
|
||||
};
|
99
src/components/TimezonePicker/index.tsx
Normal file
99
src/components/TimezonePicker/index.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { DateTime } from "luxon";
|
||||
import { useQuery } from "react-query";
|
||||
import { QueryKeys } from "../../consts";
|
||||
import { fetchUserInfo } from "../../api";
|
||||
|
||||
type DisplayProps = {
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
const TimezoneDisplay = ({ timezone }: DisplayProps) => {
|
||||
const now = DateTime.now().setZone(timezone);
|
||||
|
||||
const hour = now.hour;
|
||||
const minute = now.minute;
|
||||
|
||||
return (
|
||||
<>
|
||||
<strong>
|
||||
<span class="set-timezone">{timezone}</span>
|
||||
</strong>{" "}
|
||||
(
|
||||
<span class="set-time">
|
||||
{hour}:{minute}
|
||||
</span>
|
||||
)
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TimezonePicker = () => {
|
||||
const browserTimezone = DateTime.now().zone.name;
|
||||
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: QueryKeys.USER_DATA,
|
||||
queryFn: fetchUserInfo,
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="modal" id="chooseTimezoneModal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<label class="modal-card-title" for="urlInput">
|
||||
Update Timezone{" "}
|
||||
<a href="/help/timezone">
|
||||
<span>
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
</a>
|
||||
</label>
|
||||
<button class="delete close-modal" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p>
|
||||
Your configured timezone is:{" "}
|
||||
<TimezoneDisplay timezone={browserTimezone}></TimezoneDisplay>
|
||||
<br></br>
|
||||
<br></br>
|
||||
Your browser timezone is:{" "}
|
||||
<TimezoneDisplay timezone={browserTimezone}></TimezoneDisplay>
|
||||
<br></br>
|
||||
{!isError && (
|
||||
<>
|
||||
Your bot timezone is:{" "}
|
||||
{isLoading ? (
|
||||
<i className="fas fa-cog fa-spin"></i>
|
||||
) : (
|
||||
<TimezoneDisplay
|
||||
timezone={data.timezone || "UTC"}
|
||||
></TimezoneDisplay>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<br></br>
|
||||
<div class="has-text-centered">
|
||||
<button class="button is-success close-modal" id="set-browser-timezone">
|
||||
<span>Use Browser Timezone</span>{" "}
|
||||
<span class="icon">
|
||||
<i class="fab fa-firefox-browser"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-link close-modal" id="set-bot-timezone">
|
||||
<span>Use Bot Timezone</span>{" "}
|
||||
<span class="icon">
|
||||
<i class="fab fa-discord"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-warning close-modal" id="update-bot-timezone">
|
||||
Set Bot Timezone
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<button class="modal-close is-large close-modal" aria-label="close"></button>
|
||||
</div>
|
||||
);
|
||||
};
|
15
src/components/Welcome/index.tsx
Normal file
15
src/components/Welcome/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
export const Welcome = () => (
|
||||
<section id="welcome">
|
||||
<div class="has-text-centered">
|
||||
<p class="title">Welcome!</p>
|
||||
<p class="subtitle is-hidden-touch">Select an option from the side to get started</p>
|
||||
<p class="subtitle is-hidden-desktop">
|
||||
Press the{" "}
|
||||
<span class="icon">
|
||||
<i class="fal fa-bars"></i>
|
||||
</span>{" "}
|
||||
to get started
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
4
src/consts.ts
Normal file
4
src/consts.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum QueryKeys {
|
||||
USER_DATA = "userData",
|
||||
USER_GUILDS = "userGuilds",
|
||||
}
|
4
src/index.tsx
Normal file
4
src/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { render } from "preact";
|
||||
import { App } from "./components/App";
|
||||
|
||||
render(<App />, document.getElementById("app"));
|
Reference in New Issue
Block a user