Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation Overhaul + Homepage #144

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ SESSION_SECRET= # A random string, in production should be a secure

# Google login configuration
GOOGLE_CLIENT_ID=
GOOGLE_PERMITTED_DOMAINS=york.ac.uk
GOOGLE_PERMITTED_DOMAINS=ystv.co.uk
# Needed for youtube integration, optional otherwise. This is *not* the client secret!
GOOGLE_API_KEY=

# AdamRMS configuration
ADAMRMS_EMAIL=
ADAMRMS_PASSWORD=
ADAMRMS_BASE=
ADAMRMS_PROJECT_TYPE_ID=

# Slack configuration
SLACK_ENABLED=false
SESSION_SECRET=
SLACK_ENABLED=
SLACK_BOT_TOKEN=
SLACK_APP_TOKEN=
SLACK_SIGNING_SECRET=
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_TEAM_ID= # Should be set only if the slack integration is used across multiple workspaces
SLACK_USER_FEEDBACK_CHANNEL=
SENTRY_PROJECT_ID=
# SLACK_BOT_TOKEN=
# SLACK_APP_TOKEN=
# SLACK_SIGNING_SECRET=
# SLACK_CLIENT_ID=
# SLACK_CLIENT_SECRET=
# Should be set if the slack integration is used across multiple workspaces
SLACK_TEAM_ID=
# Use https://www.streamweasels.com/tools/youtube-channel-id-and-user-id-convertor/ to convert
YOUTUBE_CHANNEL_ID=UCwViVJcFiwBSDmzhaHiagqw
18 changes: 10 additions & 8 deletions app/(authenticated)/calendar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default async function CalendarPage({

return (
<>
<h1 className={"text-4xl font-bold"}>Calendar</h1>
{vacantEventsCount > 0 && (
<Alert
variant={"outline"}
Expand All @@ -125,14 +126,15 @@ export default async function CalendarPage({
</Button>
</Alert>
)}
<div className={"flex items-end justify-between"}>
<h1 className={"text-4xl font-bold"}>YSTV Calendar</h1>
<PermissionGate required={calendarEditPermissions}>
<Button component={Link} href="/calendar/new" fz="md">
Add Event
</Button>
</PermissionGate>
</div>

<PermissionGate required={calendarEditPermissions}>
<br />
</PermissionGate>
<PermissionGate required={calendarEditPermissions}>
<Button component={Link} href="/calendar/new" fz="md" my="md">
Add Event
</Button>
</PermissionGate>
<PermissionGate required={calendarEditPermissions}>
<br />
</PermissionGate>
Expand Down
22 changes: 11 additions & 11 deletions app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Image from "next/image";
import Logo from "@/app/_assets/logo.png";
import Link from "next/link";
import { UserProvider } from "@/components/UserContext";
import { getCurrentUserOrNull } from "@/lib/auth/server";
import { getCurrentUser, mustGetCurrentUser } from "@/lib/auth/server";
import YSTVBreadcrumbs from "@/components/Breadcrumbs";
import * as Sentry from "@sentry/nextjs";
import { UserMenu } from "@/components/UserMenu";
Expand All @@ -12,16 +11,22 @@ import { WebsocketProvider } from "@/components/WebsocketProvider";
import { useCreateSocket } from "@/lib/socket";
import { FeedbackPrompt } from "@/components/FeedbackPrompt";
import Nav from "@/components/Nav";
import { NotLoggedIn } from "@/lib/auth/errors";
import { redirect } from "next/navigation";

export default async function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getCurrentUserOrNull();

if (typeof user == "string") {
return <LoginPrompt />;
let user;
try {
user = await getCurrentUser();
} catch (e) {
if (e instanceof NotLoggedIn) {
redirect("/login");
}
throw e;
}

Sentry.setUser({
Expand All @@ -37,11 +42,6 @@ export default async function AuthenticatedLayout({
<main className="mx-2 max-w-[min(theme(maxWidth.6xl),theme(maxWidth.full))] overflow-x-hidden md:mx-6 [@media(min-width:calc(theme(maxWidth.6xl)+theme(margin.6)*2))]:mx-auto">
{children}
</main>
<br />
<footer className="mt-8 text-center text-sm text-gray-500">
Calendar version {process.env.NEXT_PUBLIC_RELEASE}. Built and
maintained by the YSTV Computing Team. <FeedbackPrompt />
</footer>
<style
dangerouslySetInnerHTML={{
__html: `
Expand Down
3 changes: 3 additions & 0 deletions app/(authenticated)/news/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function NewsPage() {
return <p>Hello There!</p>;
}
166 changes: 166 additions & 0 deletions app/(authenticated)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { getCurrentUser } from "@/lib/auth/server";
import { YouTubeEmbed } from "./youtubeEmbed";
import { Suspense } from "react";
import * as News from "@/features/news";
import * as YouTube from "@/features/youtube";
import * as Calendar from "@/features/calendar";
import { getUserName } from "@/components/UserHelpers";
import { DateTime } from "@/components/DateTimeHelpers";
import { Button, Paper, ScrollArea, Title, Text } from "@mantine/core";
import { isSameDay } from "date-fns";
import { TbArticle } from "react-icons/tb";
import { PermissionGate } from "@/components/UserContext";
import Link from "next/link";

async function YouTubeTile() {
if (!YouTube.isEnabled()) {
return null;
}
const video = await YouTube.getLatestUpload();
if (!video) {
return null;
}
return (
<div>
<Title order={2}>Latest Upload</Title>
<div className="my-10 aspect-video w-full">
<YouTubeEmbed
id={video.id!.videoId!}
title={video.snippet?.title ?? ""}
poster="hqdefault"
/>
</div>
</div>
);
}

async function NewsRow() {
const newsItem = await News.getLatestNewsItem();
if (!newsItem) {
return null;
}
return (
<Paper shadow="md" withBorder className="p-3">
<Title order={3}>{newsItem.title}</Title>
{newsItem.content.split("\n").map((line, idx) => (
<Text key={idx}>{line}</Text>
))}
<small>
Posted by {getUserName(newsItem.author)},{" "}
<DateTime val={newsItem.time.toISOString()} format="datetime" />
</small>
</Paper>
);
}

async function ProductionsNeedingCrew() {
const prods = await Calendar.listVacantEvents({});
if (!prods.events.length) {
return null;
}
return (
<div>
<Title order={2}>{prods.events.length} productions need crew</Title>
<ScrollArea h={500} className="mt-10">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{prods.events.map((event) => (
<Paper
key={event.event_id}
shadow="sm"
radius="md"
withBorder
className="flex flex-col p-4"
>
<h3 className="m-0">{event.name}</h3>
<p className="m-0 mb-2 text-sm">
<strong>
<DateTime
val={event.start_date.toISOString()}
format="datetime"
/>
{" - "}
{isSameDay(event.start_date, event.end_date) ? (
<DateTime
val={event.end_date.toISOString()}
format="time"
/>
) : (
<DateTime
val={event.end_date.toISOString()}
format="datetime"
/>
)}
</strong>
</p>
{event.signup_sheets
.filter((sheet) => sheet.crews.length > 0)
.map((sheet) => (
<div key={sheet.signup_id}>
<h3 className="m-0 text-lg">{sheet.title}</h3>
<p className="m-0 text-xs">
<DateTime
val={sheet.arrival_time.toISOString()}
format="datetime"
/>{" "}
-{" "}
{isSameDay(sheet.arrival_time, sheet.end_time) ? (
<DateTime
val={sheet.end_time.toISOString()}
format="time"
/>
) : (
<DateTime
val={sheet.end_time.toISOString()}
format="datetime"
/>
)}
</p>
{sheet.crews.map((crew) => (
<div key={crew.crew_id}>
<li className="ml-6 text-base">
{crew.positions.name}
</li>
</div>
))}
</div>
))}
<div className="mt-auto flex grow items-end justify-end">
<Button
component={Link}
href={`/calendar/${event.event_id}`}
leftSection={<TbArticle />}
>
Event Details
</Button>
</div>
</Paper>
))}
</div>
</ScrollArea>
</div>
);
}

export default async function HomePage() {
const me = await getCurrentUser();
return (
<div>
<Title order={1} className="my-10">
Welcome back, {me.first_name}!
</Title>
<div className="my-5">
<Suspense fallback={<div>Loading...</div>}>
<NewsRow />
</Suspense>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Suspense fallback={<div>Loading...</div>}>
<ProductionsNeedingCrew />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<YouTubeTile />
</Suspense>
</div>
</div>
);
}
46 changes: 46 additions & 0 deletions app/(authenticated)/user/[id]/UserPreferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Group,
Divider,
InputLabel,
Center,
VisuallyHidden,
} from "@mantine/core";
import {
ReactNode,
Expand All @@ -18,6 +20,8 @@ import {
import { changePreference } from "./actions";
import { notifications } from "@mantine/notifications";
import { useWebsocket } from "@/components/WebsocketProvider";
import { useMantineColorScheme } from "@mantine/core";
import { LuLaptop, LuMoon, LuSun } from "react-icons/lu";

type ReqPrefs = Required<PrismaJson.UserPreferences>;

Expand Down Expand Up @@ -96,8 +100,50 @@ function SegmentedPreference<K extends "timeFormat" | "icalFilter">(
}

export function UserPreferences(props: { value: ReqPrefs; userID: number }) {
const { setColorScheme, colorScheme } = useMantineColorScheme();

return (
<Stack>
<InputWrapper>
<Group>
<InputLabel>Color Scheme</InputLabel>
<SegmentedControl
value={colorScheme}
onChange={setColorScheme}
className="ml-auto min-w-[10rem]"
data={[
{
value: "light",
label: (
<Center>
<LuSun className="scale-150" aria-label="light mode" />
<VisuallyHidden>Light Mode</VisuallyHidden>
</Center>
),
},
{
value: "auto",
label: (
<Center>
<LuLaptop className="scale-150" aria-label="auto mode" />
<VisuallyHidden>Auto Mode</VisuallyHidden>
</Center>
),
},
{
value: "dark",
label: (
<Center>
<LuMoon className="scale-150" aria-label="dark mode" />
<VisuallyHidden>Dark Mode</VisuallyHidden>
</Center>
),
},
]}
/>
</Group>
</InputWrapper>
<Divider className="border-[--mantine-color-dark-4]" />
<SegmentedPreference
label="Time Format"
field="timeFormat"
Expand Down
8 changes: 8 additions & 0 deletions app/(authenticated)/youtubeEmbed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client";

import LiteYouTubeEmbed, { LiteYouTubeProps } from "react-lite-youtube-embed";
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";

export function YouTubeEmbed(props: LiteYouTubeProps) {
return <LiteYouTubeEmbed {...props} />;
}
15 changes: 0 additions & 15 deletions app/page.tsx

This file was deleted.

Loading
Loading