From c30a31e474e76c76d5b937760a51324941ad79df Mon Sep 17 00:00:00 2001
From: Ewan Lyon
Date: Sun, 5 May 2024 14:08:56 +1000
Subject: [PATCH] new schedule page
---
.../migration.sql | 3 +
apps/keystone/schema.graphql | 10 +
apps/keystone/schema.prisma | 2 +
apps/keystone/src/schema/event.ts | 9 +-
.../components/Footer/Footer.module.scss | 1 +
.../Heroblock/Heroblock.module.scss | 4 +
.../nextjs/components/Heroblock/Heroblock.tsx | 3 +-
apps/nextjs/next.config.js | 2 +-
apps/nextjs/pages/[event]/schedule.tsx | 545 ++++++++++++------
apps/nextjs/pages/index.tsx | 16 +-
apps/nextjs/styles/Schedule.event.module.scss | 290 ++++++++--
apps/nextjs/styles/colors.scss | 27 +
apps/nextjs/styles/img/icons/console.svg | 3 +
apps/nextjs/styles/img/icons/runner.svg | 3 +
apps/nextjs/styles/img/icons/stopwatch.svg | 3 +
package-lock.json | 20 +
package.json | 8 +-
17 files changed, 706 insertions(+), 243 deletions(-)
create mode 100644 apps/keystone/migrations/20240505034646_added_external_schedule_links/migration.sql
create mode 100644 apps/nextjs/styles/img/icons/console.svg
create mode 100644 apps/nextjs/styles/img/icons/runner.svg
create mode 100644 apps/nextjs/styles/img/icons/stopwatch.svg
diff --git a/apps/keystone/migrations/20240505034646_added_external_schedule_links/migration.sql b/apps/keystone/migrations/20240505034646_added_external_schedule_links/migration.sql
new file mode 100644
index 0000000..da28090
--- /dev/null
+++ b/apps/keystone/migrations/20240505034646_added_external_schedule_links/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Event" ADD COLUMN "horaro" TEXT NOT NULL DEFAULT '',
+ADD COLUMN "oengus" TEXT NOT NULL DEFAULT '';
diff --git a/apps/keystone/schema.graphql b/apps/keystone/schema.graphql
index 0b04d6c..d84c42a 100644
--- a/apps/keystone/schema.graphql
+++ b/apps/keystone/schema.graphql
@@ -684,6 +684,8 @@ type Event {
submissionInstructions: Event_submissionInstructions_Document
eventPage: Event_eventPage_Document
scheduleBlocks: JSON
+ horaro: String
+ oengus: String
}
type Event_postEventPage_Document {
@@ -767,6 +769,8 @@ input EventWhereInput {
tickets: TicketManyRelationFilter
volunteer: VolunteerManyRelationFilter
donationIncentives: IncentiveManyRelationFilter
+ horaro: StringFilter
+ oengus: StringFilter
}
input FloatNullableFilter {
@@ -801,6 +805,8 @@ input EventOrderByInput {
startDate: OrderDirection
endDate: OrderDirection
raised: OrderDirection
+ horaro: OrderDirection
+ oengus: OrderDirection
}
input EventUpdateInput {
@@ -832,6 +838,8 @@ input EventUpdateInput {
submissionInstructions: JSON
eventPage: JSON
scheduleBlocks: JSON
+ horaro: String
+ oengus: String
}
input IncentiveRelateToManyForUpdateInput {
@@ -888,6 +896,8 @@ input EventCreateInput {
submissionInstructions: JSON
eventPage: JSON
scheduleBlocks: JSON
+ horaro: String
+ oengus: String
}
input IncentiveRelateToManyForCreateInput {
diff --git a/apps/keystone/schema.prisma b/apps/keystone/schema.prisma
index 216fcef..b79e413 100644
--- a/apps/keystone/schema.prisma
+++ b/apps/keystone/schema.prisma
@@ -138,6 +138,8 @@ model Event {
submissionInstructions Json @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]")
eventPage Json @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]")
scheduleBlocks Json?
+ horaro String @default("")
+ oengus String @default("")
from_Post_event Post[] @relation("Post_event")
from_Role_event Role[] @relation("Role_event")
}
diff --git a/apps/keystone/src/schema/event.ts b/apps/keystone/src/schema/event.ts
index d019bc8..dd2271f 100644
--- a/apps/keystone/src/schema/event.ts
+++ b/apps/keystone/src/schema/event.ts
@@ -126,6 +126,13 @@ export const Event: Lists.Event = list({
},
componentBlocks: liveEventComponentBlocks,
}),
- scheduleBlocks: scheduleBlocks(),
+ ...group({
+ label: "Schedule Metadata",
+ fields: {
+ scheduleBlocks: scheduleBlocks(),
+ horaro: text(),
+ oengus: text(),
+ }
+ }),
}
});
diff --git a/apps/nextjs/components/Footer/Footer.module.scss b/apps/nextjs/components/Footer/Footer.module.scss
index 1d96ff0..98fd8d2 100644
--- a/apps/nextjs/components/Footer/Footer.module.scss
+++ b/apps/nextjs/components/Footer/Footer.module.scss
@@ -15,6 +15,7 @@
ul {
justify-content: center;
+ flex-wrap: wrap;
@include breakpoint($sm-zero-only) {
flex-direction: column;
diff --git a/apps/nextjs/components/Heroblock/Heroblock.module.scss b/apps/nextjs/components/Heroblock/Heroblock.module.scss
index b770606..4264950 100644
--- a/apps/nextjs/components/Heroblock/Heroblock.module.scss
+++ b/apps/nextjs/components/Heroblock/Heroblock.module.scss
@@ -37,6 +37,10 @@
}
}
+ p {
+ text-wrap: balance;
+ }
+
.ctaBlock {
text-align: left;
padding: 25px;
diff --git a/apps/nextjs/components/Heroblock/Heroblock.tsx b/apps/nextjs/components/Heroblock/Heroblock.tsx
index a745af0..aaef5fe 100644
--- a/apps/nextjs/components/Heroblock/Heroblock.tsx
+++ b/apps/nextjs/components/Heroblock/Heroblock.tsx
@@ -112,8 +112,7 @@ const HeroBlock = ({ event, tagLine, darkText, schedule, submitRuns, ticketLink
- {tagLine ??
- "We will be at The Game Expo! The schedule has been released!"}
+ {tagLine}
-
-
-
- Schedule Key
-
- Time
- Game
- Category
- Runners
- Estimate (HH:MM:SS)
- Platform
- Donation Incentive
-
-
- {currentRunIndex > -1 && (
-
- )}
-
- {generateRunItems(event.runs, settings, event.eventTimezone, scheduleBlocks)}
-
-
-
+
+
+
+ {currentRunIndex > -1 && (
+
+ )}
+
+
+ {getAllDays(runsWithOrder, event.eventTimezone, settings.showLocalTime).map((day) => {
+ const date = new Date(day);
+ return (
+
+ );
+ })}
+
+ {generateRunItems(runsWithOrder, settings, event.eventTimezone, scheduleBlocks)}
+
@@ -380,13 +433,8 @@ function generateSubmissionBlockMap(blocks: Block[], allRuns: QUERY_EVENT_RESULT
return blockRunMap;
}
-function getRunDate(scheduledTime: string, localTime?: string) {
- return parseInt(
- new Date(scheduledTime).toLocaleDateString(undefined, {
- timeZone: localTime,
- day: "numeric",
- }),
- );
+function runEstimateToSeconds(estimate: string) {
+ return estimate.split(":").reduce((acc, time) => 60 * acc + parseInt(time), 0);
}
enum BorderState {
@@ -395,116 +443,215 @@ enum BorderState {
IN_BLOCK,
}
-function generateRunItems(
- sortedRuns: QUERY_EVENT_RESULTS["event"]["runs"],
- settings: typeof SETTINGS,
- eventTimezone: string,
- blocks: Map
,
-) {
- let prevDate = 0;
+function getAllDays(runs: Run[], eventTimezone: string, showLocalTime: boolean) {
+ let days: string[] = [];
- const filteredRuns = FilterRuns(sortedRuns, settings.filter);
- const runs: JSX.Element[] = [];
-
- let blockRuns: JSX.Element[] = [];
- let scheduleBlockData: Block | undefined;
- let removedBorder: BorderState = BorderState.KEEP_BORDER;
- let previousBlockData: Block | undefined;
-
- for (let index = 0; index < filteredRuns.length; index++) {
- const run = filteredRuns[index];
- const runDate = getRunDate(run.scheduledTime, settings.showLocalTime ? eventTimezone : undefined);
- const nextRunDate = filteredRuns[index + 1]
- ? getRunDate(filteredRuns[index + 1].scheduledTime, settings.showLocalTime ? eventTimezone : undefined)
- : runDate;
-
- scheduleBlockData = blocks.get(run.id);
-
- // Check if we are on a new block, if we are in one check if we were previously in one and post it
- if (scheduleBlockData !== previousBlockData && previousBlockData) {
- runs.push(
-
- {blockRuns}
- ,
- );
-
- // Reset for next block
- blockRuns = [];
+ for (let i = 0; i < runs.length; i++) {
+ const runDate = new Date(runs[i].scheduledTime).toLocaleString("en-US", {
+ timeZone: showLocalTime ? eventTimezone : undefined,
+ day: "numeric",
+ month: "numeric",
+ year: "numeric",
+ });
+
+ if (!days.includes(runDate)) {
+ days.push(runDate);
}
+ }
- if (scheduleBlockData) removedBorder = BorderState.KEEP_BORDER;
- if (!scheduleBlockData && blocks.get(filteredRuns[index + 1]?.id)) removedBorder = BorderState.REMOVE_BORDER;
+ const sortedDays = days.sort((a, b) => {
+ const dateA = new Date(a);
+ const dateB = new Date(b);
+ return dateA.getTime() - dateB.getTime();
+ });
- if (prevDate !== runDate) {
- // End block if we're at a new day
- // And check that we have runs to post as well... Goodbye Reginald o7
- if (scheduleBlockData && blockRuns.length > 0) {
- runs.push({blockRuns});
- blockRuns = [];
- }
+ return days;
+}
- runs.push(
- ,
- );
- }
+function runsToDays(runs: Run[], eventTimezone: string, showLocalTime: boolean) {
+ let days: Record = {};
- (scheduleBlockData ? blockRuns : runs).push(
- ,
- );
+ for (let i = 0; i < runs.length; i++) {
+ const run = runs[i];
+ const runDate = new Date(run.scheduledTime).toLocaleString("en-US", {
+ timeZone: showLocalTime ? eventTimezone : undefined,
+ day: "numeric",
+ month: "numeric",
+ year: "numeric",
+ });
- prevDate = runDate;
- previousBlockData = scheduleBlockData;
+ if (runDate in days) {
+ days[runDate].push(run);
+ } else {
+ days[runDate] = [run];
+ }
}
- if (blockRuns.length > 0 && previousBlockData) {
- runs.push(
-
- {blockRuns}
- ,
- );
- }
+ const sortedDays = Object.entries(days).sort((a, b) => {
+ const dateA = new Date(a[0]);
+ const dateB = new Date(b[0]);
+ return dateA.getTime() - dateB.getTime();
+ });
- return runs;
+ return sortedDays.map(([day, runs]) => ({
+ day,
+ runs,
+ }));
+}
+
+const RunHover = styled(({ className, odd, block, ...props }: TooltipProps & { odd: boolean; block?: Block }) => (
+
+))(({ odd, block }) => ({
+ [`& .${tooltipClasses.tooltip}`]: {
+ backgroundColor: block?.colour ? `${block.colour}a6` : odd ? "#cc7722a6" : "#437c90a6",
+ color: block?.textColour ? block.textColour : "#fff",
+ border: `1px solid ${block?.colour ? block.colour : odd ? "#cc7722" : "#437c90"}`,
+ backdropFilter: "blur(10px)",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ maxWidth: 500,
+ },
+}));
+
+function generateRunItems(
+ sortedRuns: Run[],
+ settings: typeof SETTINGS,
+ eventTimezone: string,
+ blocks: Map,
+) {
+ const filteredRuns = FilterRuns(sortedRuns, settings.filter);
+ const runDays = runsToDays(filteredRuns, eventTimezone, settings.showLocalTime);
+
+ return (
+
+ {runDays.map(({ day, runs }, i) => {
+ let yesterdayRunTime = 0;
+
+ if (i != 0) {
+ const yesterdaysFinalRun = runDays[i - 1].runs.at(-1);
+
+ if (yesterdaysFinalRun) {
+ const endOfYesterdayRun = addSeconds(
+ new Date(yesterdaysFinalRun.scheduledTime),
+ runEstimateToSeconds(yesterdaysFinalRun.estimate),
+ );
+
+ const startOfFirstRun = new Date(runs[0].scheduledTime);
+
+ const timeBetweenRuns = new Date(startOfFirstRun.getTime() - endOfYesterdayRun.getTime());
+ const timeBetweenRunsSeconds = timeBetweenRuns.getTime() / (1000 * 60);
+
+ if (timeBetweenRunsSeconds <= 30) {
+ // Within 30 minutes, calculate the time difference to add to the start of the day
+ const startOfToday = new Date(
+ startOfFirstRun.getFullYear(),
+ startOfFirstRun.getMonth(),
+ startOfFirstRun.getDate(),
+ );
+ const yesterdayRunExtraTime = endOfYesterdayRun.getTime() - startOfToday.getTime();
+ yesterdayRunTime = yesterdayRunExtraTime / 1000;
+ }
+ }
+ }
+
+ const totalSeconds =
+ runs.reduce((acc, run) => acc + Math.max(runEstimateToSeconds(run.estimate), 300), 0) +
+ yesterdayRunTime;
+ const runDay = new Date(day);
+
+ return (
+
+
+
+
+ {yesterdayRunTime > 0 && (
+
+ )}
+ {runs.map((run, i) => {
+ let width =
+ (Math.max(runEstimateToSeconds(run.estimate), 300) / totalSeconds) * 100;
+
+ if (i == runs.length - 1) {
+ // If the run goes to the next day, subtract the overlap
+
+ const runScheduledTime = new Date(run.scheduledTime);
+ const finalRunEndTime = addSeconds(
+ runScheduledTime,
+ runEstimateToSeconds(run.estimate),
+ );
+
+ const nextDay = new Date(
+ runScheduledTime.getFullYear(),
+ runScheduledTime.getMonth(),
+ runScheduledTime.getDate(),
+ );
+
+ nextDay.setDate(nextDay.getDate() + 1);
+
+ if (nextDay.getDate() === finalRunEndTime.getDate()) {
+ const overlapTime = nextDay.getTime() - finalRunEndTime.getTime();
+ const overlapSeconds = overlapTime / 1000;
+ width =
+ ((runEstimateToSeconds(run.estimate) - overlapSeconds) / totalSeconds) *
+ 100;
+ }
+ }
+
+ return ;
+ })}
+
+
+ {runs.map((run) => (
+
+ ))}
+
+
+
+ );
+ })}
+
+ );
}
interface DateDividerProps {
date: Date;
- showLocalTime: boolean;
- eventTimezone: string;
}
const DateDivider: React.FC = (props: DateDividerProps) => {
- const dateString = props.showLocalTime
- ? formatInTimeZone(props.date, props.eventTimezone, "EEEE do, MMMM")
- : format(props.date, "EEEE do, MMMM");
- return {dateString}
;
+ const dateString = format(props.date, "EEEE do, MMMM");
+ return (
+ window.scrollTo({ top: 0, behavior: "smooth" })}>
+ {dateString}
+
+ );
};
interface RunItemProps {
- run: QUERY_EVENT_RESULTS["event"]["runs"][0];
+ run: Run;
showLocalTime: boolean;
eventTimezone: string;
isLive?: boolean;
+ block?: Block;
style?: React.CSSProperties;
}
// Runner parsing
-function runnerParsing(runnersArray: QUERY_EVENT_RESULTS["event"]["runs"][0]["runners"]) {
+function runnerParsing(runnersArray: Run["runners"]) {
+ if (runnersArray.length == 0) {
+ return <>???>;
+ }
+
return (
{runnersArray.map((runner, i) => {
@@ -559,7 +706,7 @@ const RunItem: React.FC
= (props: RunItemProps) => {
})
: new Date(run.scheduledTime).toLocaleTimeString("en-AU", runItemOptions);
- if (convertedTimezone[0] === "0") convertedTimezone = " " + convertedTimezone.substring(1);
+ if (convertedTimezone[0] === "0") convertedTimezone = convertedTimezone.substring(1);
if (run.game === "Setup Buffer") {
return (
@@ -570,8 +717,8 @@ const RunItem: React.FC = (props: RunItemProps) => {
}
let categoryExtras = <>>;
- if (run.race) categoryExtras = RACE ;
- if (run.coop) categoryExtras = CO-OP ;
+ if (run.race) categoryExtras = RACE;
+ if (run.coop) categoryExtras = CO-OP;
const runClassNames = [styles.run];
if (props.isLive) runClassNames.push(styles.liveRun);
@@ -579,28 +726,104 @@ const RunItem: React.FC = (props: RunItemProps) => {
const estimateSplit = run.estimate.split(":");
const hours = parseInt(estimateSplit[0]);
const minutes = parseInt(estimateSplit[1]);
- const formattedHours = hours === 0 ? " 0" : hours < 10 ? ` ${hours}` : hours.toString().padStart(2, " ");
+ const formattedHours = hours.toString();
const formattedMinutes = minutes.toString().padStart(2, "0");
const estimateText = `${formattedHours}:${formattedMinutes}:00`;
+ // Bad hardcoding bad!
+ let overwriteFilter;
+ if (props.block) {
+ if (props.block.textColour === "#ffffff") {
+ overwriteFilter = "invert(100%)";
+ } else {
+ overwriteFilter = "unset";
+ }
+ }
+
return (
-
-
{convertedTimezone}
+
+
+ {convertedTimezone}
+
+ {props.block?.name &&
{props.block?.name}}
{run.game}
-
+
{categoryExtras}
{run.category}
- {run.runners.length > 0 ? runnerParsing(run.runners) : run.racer}
- {estimateText}
- {run.platform}
-
- {run.donationIncentiveObject?.map((incentive) => incentive.title).join(" | ")}
-
+
+
+
+ {run.runners.length > 0 ? runnerParsing(run.runners) : run.racer}
+
+
+
+ {estimateText}
+
+
+
+ {run.platform}
+
+
+ {run.donationIncentiveObject && run.donationIncentiveObject.length > 0 && (
+
+
+ Incentives
+
+ {run.donationIncentiveObject?.map((incentive) => (
+
+ {incentive.title}
+
+ ))}
+
+ )}
);
};
+const RunVisualiser = ({ run, proportion, block }: { run: Run; proportion: number; block?: Block }) => {
+ const oddRun = run.order % 2 != 0;
+
+ return (
+
+ {block && {block.name}
}
+ {run.game}
+ {run.category}
+
+
+ {run.runners.length > 0 ? runnerParsing(run.runners) : run.racer}
+
+
+ }
+ run-odd={oddRun.toString()}
+ odd={oddRun}
+ block={block}>
+ {
+ const runElement = document.querySelector(`#${run.id}`);
+ if (runElement != null) {
+ runElement.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ }}
+ />
+
+ );
+};
+
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const ssrCache = ssrExchange({ isClient: false });
const client = initUrqlClient(
@@ -609,7 +832,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
process.env.NODE_ENV === "production"
? "https://keystone.ausspeedruns.com/api/graphql"
: "http://localhost:8000/api/graphql",
- exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
+ exchanges: [cacheExchange, ssrCache, fetchExchange],
},
false,
);
diff --git a/apps/nextjs/pages/index.tsx b/apps/nextjs/pages/index.tsx
index e5da534..ca4af34 100644
--- a/apps/nextjs/pages/index.tsx
+++ b/apps/nextjs/pages/index.tsx
@@ -106,22 +106,10 @@ export default function Home() {
{/*
*/}
- {/*
*/}
span {
- padding-bottom: 4px;
- }
+.externalSchedules {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
- .scheduleHeader {
- display: grid;
- grid-template-columns: min-content repeat(3, 1fr);
- border: 1px solid $secondary;
- border-radius: 5px;
+.day {
+ display: grid;
+ // grid-template-columns: 10% 90%;
+ gap: 16px;
+}
- span {
- padding: 8px;
- }
+.visualiser {
+ display: flex;
+ // flex-direction: column;
+ gap: 1px;
+ margin-bottom: 1rem;
- .topRow {
- border-bottom: 1px solid $secondary;
- }
+ .visualiserRun {
+ cursor: pointer;
+ height: 40px;
+ flex-grow: 1;
- .notLast {
- border-right: 1px solid $secondary;
+ &[run-odd="true"] {
+ background: $primary;
}
- .donationIncentive {
- grid-column: 3 / 5;
- font-style: italic;
+ &[run-odd="false"] {
+ background: $secondary;
}
}
}
-.localTimeToggle {
- margin: 0.5rem auto !important;
+.visualiserTooltip {
+ // cursor: pointer;
+
+ * {
+ text-align: center;
+ text-wrap: balance;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ h3 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ img {
+ filter: invert(100%);
+ }
+ }
}
.schedule {
// border: 1px solid $color-off-white;
display: flex;
flex-direction: column;
- align-items: end;
+ width: 100%;
+
+ .dayButtons {
+ display: flex;
+ gap: 8px;
+
+ @include breakpoint($sm-zero-only) {
+ flex-wrap: wrap;
+ }
+ }
.dateDivider {
- max-width: $normalWidth;
width: 100%;
- border-bottom: 5px solid $primary;
+ border-bottom: 2px solid $primary;
color: $secondary;
font-weight: bold;
font-size: 1.3rem;
padding: 1rem;
margin-top: 1rem;
+ text-align: center;
+
+ position: sticky;
+ top: 0;
+ background: rgba(255, 255, 255, 0.808);
+ backdrop-filter: blur(10px);
+ z-index: 2;
}
.setupBuffer {
@@ -154,47 +186,185 @@ $normalWidth: 800px;
border-bottom: 2px solid $secondary;
}
+ .runs {
+ padding: 0 1rem;
+ }
+
.run {
- max-width: $normalWidth;
- width: $normalWidth;
- padding: 4px;
- display: grid;
- grid-template-columns: min-content repeat(3, 1fr);
- border-bottom: 2px solid $secondary;
+ padding: 8px;
+ margin-top: 4px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 1.1rem;
+ border-radius: 20px 16px 16px 16px;
+ position: relative;
+ width: 100%;
- &:nth-child(odd) {
- background: #f9f9f900;
+ @include breakpoint($sm-zero-only) {
+ border-radius: 16px;
+ }
+
+
+ &[run-odd="true"] {
+ background: $primary-50;
+ --icon-filter: invert(48%) sepia(57%) saturate(650%) hue-rotate(349deg) brightness(97%) contrast(89%);
+ --colour-accent: #{$primary-600};
+ --colour-full: #{$primary-200};
+ }
+
+ &[run-odd="false"] {
+ background: $secondary-50;
+ --icon-filter: invert(46%) sepia(14%) saturate(1227%) hue-rotate(150deg) brightness(94%) contrast(98%);
+ --colour-accent: #{$secondary-600};
+ --colour-full: #{$secondary-200};
}
& > span {
padding: 4px;
+ text-align: center;
+ text-wrap: balance;
}
.game {
+ font-family: Russo One;
+ font-size: 170%;
+ margin-bottom: -0.25rem;
+ max-width: 70%;
+
+ @include breakpoint($sm-zero-only) {
+ max-width: 100%;
+ }
+ }
+
+ .category {
font-weight: bold;
+ font-size: 120%;
+ margin-bottom: 0.5rem;
+ color: var(--colour-accent);
+ }
+
+ .metaData {
+ width: 100%;
+ display: grid;
+ gap: 32px;
+ grid-template-columns: 1fr auto 1fr;
+ color: var(--colour-accent);
+ padding: 0 8px;
+
+ @include breakpoint($sm-zero-only) {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 16px;
+ }
+ }
+
+ .runners,
+ .estimate,
+ .platform {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ img {
+ height: 1.5rem;
+ width: 1.5rem;
+ filter: var(--icon-filter);
+ }
+ }
+
+ .runners,
+ .platform {
+ flex-grow: 1;
+
+ @include breakpoint($sm-zero) {
+ flex-grow: 0;
+ }
+ }
+
+ .runners {
+ text-align: center;
+ text-wrap: balance;
+ justify-content: flex-end;
+
+ a {
+ color: var(--color-accent);
+ }
}
.time {
+ padding: 8px;
+ font-weight: bold;
+ font-size: 120%;
white-space: nowrap;
text-transform: uppercase;
- margin-right: 8px;
+ min-width: 115px;
+
+ position: absolute;
+ top: 0;
+ background: var(--colour-accent);
+ left: 0;
+ border-radius: 16px 0;
+ color: $light-text;
+
+ @include breakpoint($sm-zero-only) {
+ position: relative;
+ width: 100%;
+ border-radius: 8px 8px 0px 0px;
+ }
}
.categoryExtras {
font-weight: bold;
- // margin-left: -4px;
+ background: var(--colour-accent);
+ color: $light-text;
+ margin-right: 12px;
+ padding: 0 6px;
}
- a {
- color: $dark-text;
- // text-decoration: none;
- // font-weight: 600;
- // margin: 0 -2px;
+ .donationIncentives {
+ margin-top: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: var(--colour-accent);
+ border: 3px solid var(--colour-accent);
+ padding: 8px;
+ border-radius: 8px;
+
+ .title {
+ display: flex;
+ align-items: center;
+ }
}
.donationIncentive {
- grid-column: 3 / 5;
font-style: italic;
+ font-weight: bold;
+ }
+
+ .blockName {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 8px;
+ font-weight: bold;
+ font-size: 120%;
+ border: 3px solid white;
+ border-top: 0;
+ border-right: 0;
+ border-bottom-left-radius: 16px;
+
+ @include breakpoint($sm-zero-only) {
+ position: relative;
+ width: 100%;
+ border-radius: 0;
+ border-left: 3px solid white;
+ border-right: 3px solid white;
+ border-radius: 0 0 8px 8px;
+ margin-bottom: 0.5rem;
+ }
}
@include breakpoint($sm-zero-only) {
diff --git a/apps/nextjs/styles/colors.scss b/apps/nextjs/styles/colors.scss
index ca1e83a..f07561c 100644
--- a/apps/nextjs/styles/colors.scss
+++ b/apps/nextjs/styles/colors.scss
@@ -58,3 +58,30 @@ $dh-red: #FF0046;
$dh-orange-to-orange: linear-gradient(90deg, $dh-light-orange, $dh-orange);
$dh-yellow-to-orange: linear-gradient(90deg, $dh-yellow, $dh-light-orange);
$dh-orange-to-red: linear-gradient(90deg, $dh-orange, $dh-red);
+
+// Tailwind Colours
+
+$primary-50: #fdf9ed;
+$primary-100: #f7eece;
+$primary-200: #efdb98;
+$primary-300: #e7c362;
+$primary-400: #e1ad3e;
+$primary-500: #d89128;
+$primary-600: #cc7722;
+$primary-700: #9f511e;
+$primary-800: #82401e;
+$primary-900: #6b361c;
+$primary-950: #3d1a0b;
+
+$secondary-50: #f2f8f9;
+$secondary-100: #ddecf0;
+$secondary-200: #c0dae1;
+$secondary-300: #94c0cc;
+$secondary-400: #619caf;
+$secondary-500: #437c90;
+$secondary-600: #3c697e;
+$secondary-700: #365768;
+$secondary-800: #324b58;
+$secondary-900: #2e404b;
+$secondary-950: #1a2832;
+
diff --git a/apps/nextjs/styles/img/icons/console.svg b/apps/nextjs/styles/img/icons/console.svg
new file mode 100644
index 0000000..9ee4ea3
--- /dev/null
+++ b/apps/nextjs/styles/img/icons/console.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/nextjs/styles/img/icons/runner.svg b/apps/nextjs/styles/img/icons/runner.svg
new file mode 100644
index 0000000..4f9986d
--- /dev/null
+++ b/apps/nextjs/styles/img/icons/runner.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/nextjs/styles/img/icons/stopwatch.svg b/apps/nextjs/styles/img/icons/stopwatch.svg
new file mode 100644
index 0000000..6ef3145
--- /dev/null
+++ b/apps/nextjs/styles/img/icons/stopwatch.svg
@@ -0,0 +1,3 @@
+
diff --git a/package-lock.json b/package-lock.json
index 9f04654..aab7807 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10933,6 +10933,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"optional": true,
"os": [
"darwin"
@@ -10948,6 +10949,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"optional": true,
"os": [
"darwin"
@@ -10963,6 +10965,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"optional": true,
"os": [
"linux"
@@ -10978,6 +10981,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"optional": true,
"os": [
"linux"
@@ -10993,6 +10997,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"optional": true,
"os": [
"linux"
@@ -11008,6 +11013,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"optional": true,
"os": [
"linux"
@@ -11023,6 +11029,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"optional": true,
"os": [
"linux"
@@ -11038,6 +11045,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"optional": true,
"os": [
"win32"
@@ -11053,6 +11061,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"optional": true,
"os": [
"win32"
@@ -11068,6 +11077,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"optional": true,
"os": [
"win32"
@@ -35176,60 +35186,70 @@
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.102.tgz",
"integrity": "sha512-CJDxA5Wd2cUMULj3bjx4GEoiYyyiyL8oIOu4Nhrs9X+tlg8DnkCm4nI57RJGP8Mf6BaXPIJkHX8yjcefK2RlDA==",
+ "dev": true,
"optional": true
},
"@swc/core-darwin-x64": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.102.tgz",
"integrity": "sha512-X5akDkHwk6oAer49oER0qZMjNMkLH3IOZaV1m98uXIasAGyjo5WH1MKPeMLY1sY6V6TrufzwiSwD4ds571ytcg==",
+ "dev": true,
"optional": true
},
"@swc/core-linux-arm-gnueabihf": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.102.tgz",
"integrity": "sha512-kJH3XtZP9YQdjq/wYVBeFuiVQl4HaC4WwRrIxAHwe2OyvrwUI43dpW3LpxSggBnxXcVCXYWf36sTnv8S75o2Gw==",
+ "dev": true,
"optional": true
},
"@swc/core-linux-arm64-gnu": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.102.tgz",
"integrity": "sha512-flQP2WDyCgO24WmKA1wjjTx+xfCmavUete2Kp6yrM+631IHLGnr17eu7rYJ/d4EnDBId/ytMyrnWbTVkaVrpbQ==",
+ "dev": true,
"optional": true
},
"@swc/core-linux-arm64-musl": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.102.tgz",
"integrity": "sha512-bQEQSnC44DyoIGLw1+fNXKVGoCHi7eJOHr8BdH0y1ooy9ArskMjwobBFae3GX4T1AfnrTaejyr0FvLYIb0Zkog==",
+ "dev": true,
"optional": true
},
"@swc/core-linux-x64-gnu": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.102.tgz",
"integrity": "sha512-dFvnhpI478svQSxqISMt00MKTDS0e4YtIr+ioZDG/uJ/q+RpcNy3QI2KMm05Fsc8Y0d4krVtvCKWgfUMsJZXAg==",
+ "dev": true,
"optional": true
},
"@swc/core-linux-x64-musl": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.102.tgz",
"integrity": "sha512-+a0M3CvjeIRNA/jTCzWEDh2V+mhKGvLreHOL7J97oULZy5yg4gf7h8lQX9J8t9QLbf6fsk+0F8bVH1Ie/PbXjA==",
+ "dev": true,
"optional": true
},
"@swc/core-win32-arm64-msvc": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.102.tgz",
"integrity": "sha512-w76JWLjkZNOfkB25nqdWUNCbt0zJ41CnWrJPZ+LxEai3zAnb2YtgB/cCIrwxDebRuMgE9EJXRj7gDDaTEAMOOQ==",
+ "dev": true,
"optional": true
},
"@swc/core-win32-ia32-msvc": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.102.tgz",
"integrity": "sha512-vlDb09HiGqKwz+2cxDS9T5/461ipUQBplvuhW+cCbzzGuPq8lll2xeyZU0N1E4Sz3MVdSPx1tJREuRvlQjrwNg==",
+ "dev": true,
"optional": true
},
"@swc/core-win32-x64-msvc": {
"version": "1.3.102",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.102.tgz",
"integrity": "sha512-E/jfSD7sShllxBwwgDPeXp1UxvIqehj/ShSUqq1pjR/IDRXngcRSXKJK92mJkNFY7suH6BcCWwzrxZgkO7sWmw==",
+ "dev": true,
"optional": true
},
"@swc/counter": {
diff --git a/package.json b/package.json
index 7c73215..f4a3f16 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@mui/icons-material": "^5.15.4",
"@mui/material": "^5.15.4",
"@mui/x-date-pickers": "^6.19.0",
+ "@nx/plugin": "16.6.0",
"@stripe/stripe-js": "^2.3.0",
"@swc/helpers": "0.5.3",
"animejs": "^3.2.2",
@@ -60,8 +61,7 @@
"underscore": "^1.13.6",
"urql": "^4.0.6",
"uuid": "^9.0.1",
- "zod": "^3.22.4",
- "@nx/plugin": "16.6.0"
+ "zod": "^3.22.4"
},
"devDependencies": {
"@babel/preset-react": "^7.23.3",
@@ -70,6 +70,7 @@
"@nx-tools/nx-container": "^5.1.0",
"@nx/devkit": "16.6.0",
"@nx/eslint-plugin": "16.6.0",
+ "@nx/jest": "16.6.0",
"@nx/js": "16.6.0",
"@nx/linter": "16.6.0",
"@nx/next": "16.6.0",
@@ -129,7 +130,6 @@
"vite": "4.4.8",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "4.2.3",
- "vitest": "0.34.1",
- "@nx/jest": "16.6.0"
+ "vitest": "0.34.1"
}
}