Skip to content

Commit

Permalink
Add a rules page. Update mod instructions. Add email links to usernam…
Browse files Browse the repository at this point in the history
…es (admin-only).
  • Loading branch information
dmint789 committed Jul 21, 2024
1 parent c96a3c7 commit 8b3a56a
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 51 deletions.
1 change: 1 addition & 0 deletions client/app/admin/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ const CreateEditEventPage = () => {
rows={5}
disabled={loadingDuringSubmit}
/>
<p className="fs-6">You can use the bullet character (•) in the rules to mark each point.</p>
</Form>
)}

Expand Down
25 changes: 5 additions & 20 deletions client/app/competitions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ContestLayout from '@c/ContestLayout';
import ContestTypeBadge from '@c/ContestTypeBadge';
import Country from '@c/Country';
import Competitor from '@c/Competitor';
import MarkdownDescription from '@c/MarkdownDescription';
import { IContest, IContestData } from '@sh/types';
import { ContestState, ContestType } from '@sh/enums';
import { getDateOnly } from '@sh/sharedFunctions';
Expand All @@ -27,23 +28,6 @@ const ContestDetailsPage = async ({ params }: { params: { id: string } }) => {
((!contest.endDate && start.getTime() === startOfDayInLocalTZ.getTime()) ||
(contest.endDate && start <= startOfDayInLocalTZ && new Date(contest.endDate) >= startOfDayInLocalTZ));

const getFormattedDescription = () => {
// This parses links using markdown link syntax
const markdownLinkRegex = /(\[[^\]]*\]\(https?:\/\/[^)]*\))/g;
const tempString = contest.description.replace(markdownLinkRegex, ':::::$1:::::');
const output = tempString.split(':::::').map((part, index) =>
markdownLinkRegex.test(part) ? (
<a key={index} href={/\((https?:\/\/[^)]*)\)/.exec(part)[1]} target="_blank">
{/\[([^\]]*)\]/.exec(part)[1]}
</a>
) : (
part
),
);

return output;
};

return (
<ContestLayout contest={contest} activeTab="details">
<div className="row w-100 mb-4 mx-0 fs-5">
Expand Down Expand Up @@ -110,9 +94,10 @@ const ContestDetailsPage = async ({ params }: { params: { id: string } }) => {
<p className="mb-4">The results for this contest are currently being checked</p>
) : undefined}
{contest.description && (
<p className="lh-base" style={{ whiteSpace: 'pre-wrap' }}>
<b>Description:</b>&#8194;{getFormattedDescription()}
</p>
<>
<p className="fw-bold">Description:</p>
<MarkdownDescription>{contest.description}</MarkdownDescription>
</>
)}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/ContestLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const ContestLayout = ({

return (
<div>
<h2 className="px-2 text-center lh-base">{contest.name}</h2>
<h2 className="px-2 text-center">{contest.name}</h2>
<Tabs tabs={tabs} activeTab={activeTab} forServerSidePage />

{children}
Expand Down
6 changes: 4 additions & 2 deletions client/app/components/CreatorDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { IFeUser } from '@sh/types';

const CreatorDetails = ({ creator }: { creator: IFeUser }) => {
if (creator) {
const username = <a href={`mailto:${creator.email}`}>{creator.username}</a>;

return creator.person ? (
<>
<Competitor person={creator.person} />
<span>(username: {creator.username})</span>
<span>(username: {username})</span>
</>
) : (
<span>{creator.username}</span>
<span>{username}</span>
);
}

Expand Down
20 changes: 20 additions & 0 deletions client/app/components/MarkdownDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const MarkdownDescription = ({ children }: { children: React.ReactNode }) => {
const markdownLinkRegex = /(\[[^\]]*\]\(https?:\/\/[^)]*\))/g;
const tempString = children.toString().replace(markdownLinkRegex, ':::::$1:::::');

return (
<p style={{ whiteSpace: 'pre-wrap' }}>
{tempString.split(':::::').map((part, index) =>
markdownLinkRegex.test(part) ? (
<a key={index} href={/\((https?:\/\/[^)]*)\)/.exec(part)[1]} target="_blank">
{/\[([^\]]*)\]/.exec(part)[1]}
</a>
) : (
part
),
)}
</p>
);
};

export default MarkdownDescription;
9 changes: 9 additions & 0 deletions client/app/components/UI/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ const NavbarItems = () => {
</li>
</ul>
</li>
<li className="nav-item">
<Link
className={`nav-link ${pathname === '/rules' ? ' active' : ''}`}
href="/rules"
onClick={collapseAll}
>
Rules
</Link>
</li>
{!userInfo ? (
<li className="nav-item">
<Link className="nav-link" href="/login" onClick={collapseAll}>
Expand Down
9 changes: 5 additions & 4 deletions client/app/moderator-instructions/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ const ModeratorInstructions = ({ children }: { children: React.ReactNode }) => {

<div className="mb-4">
<p>
If you would like to hold unofficial events at a WCA competition (A) or create an unofficial competition (B)
or meetup (C), follow these steps:
If you don't already have moderator privileges and you would like to hold unofficial events at a WCA
competition (A) or create an unofficial competition (B) or meetup (C), follow these steps:
</p>
<div style={{ height: '1rem' }} />
<p>
1. <Link href="/register">Create an account</Link> and send an email to {C.contactEmail} with this
information:
1. <Link href="/register">Create an account</Link> and send an email to {C.contactEmail} with the following
information (exception: for WCA competitions,{' '}
<b>you must first wait until the competition has been announced</b> on the WCA website):
</p>
<p>1.1. Username</p>
<p>1.2. WCA ID</p>
Expand Down
4 changes: 0 additions & 4 deletions client/app/moderator-instructions/meetup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ const page = () => {
<p>
C3. The rest of the process is the same as <Link href="wca">{tabs[0].title}</Link>.
</p>
<p>
C4. A meetup may not have fewer than three competitors in total. Such meetups will be removed from Cubing
Contests without being published.
</p>
</div>
</div>
);
Expand Down
4 changes: 0 additions & 4 deletions client/app/moderator-instructions/unofficial/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ const page = () => {
competitions. Please <b>DO NOT</b> attempt to use Cubing Contests as a substitute for the WCA. Your
competition may be rejected if it is deemed that it could be held as a WCA competition instead.
</p>
<p>
B4. An unofficial competition may not have fewer than three competitors in total. Such competitions will be
removed from Cubing Contests without being published.
</p>
</div>
</div>
);
Expand Down
28 changes: 12 additions & 16 deletions client/app/moderator-instructions/wca/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Tabs from '@c/UI/Tabs';
import Link from 'next/link';
import { tabs } from '~/app/moderator-instructions/tabs';

const page = () => {
Expand All @@ -11,7 +12,9 @@ const page = () => {
A1. If you are holding unofficial events at a WCA competition, first you must wait until it's approved on the
WCA website. Then you can click on <b className="hl">Create new contest</b>, enter the ID of the competition
from the WCA website, select the <b className="hl">WCA Competition</b> contest type, and click{' '}
<b className="hl">Get WCA competition details</b>.
<b className="hl">Get WCA competition details</b>. The website may return an error, saying that the
competition is not found, if not enough time has passed after it got published on the WCA website. In that
case, simply try again the following day.
</p>
<p>
A2. Edit the editable fields, if necessary. All users with moderator privileges who are listed as organizers
Expand Down Expand Up @@ -39,21 +42,14 @@ const page = () => {
Contests page on the website. You may still edit some of the details after creation, if necessary.
</p>
<p>
A6. You can use the{' '}
<a href="https://experiments.cubing.net/cubing.js/mark3/" target="_blank">
Cubing JS scramble generator
</a>{' '}
or{' '}
<a href="https://cstimer.net/" target="_blank">
csTimer
</a>{' '}
to generate the scrambles (many non-WCA puzzles are also supported). To generate the scorecards, you can click{' '}
<b className="hl">Edit</b> and click <b className="hl">Scorecards</b> (this button becomes available after the
contest gets approved). Since there is no registration through Cubing Contests yet, the names won't be filled
in and there are no groups, but there is one page for each round of each event. You can print as many copies
of each page as you need for the corresponding rounds, and ask the competitors to fill their names in by hand
when submitting their puzzles. Keep in mind that all names must be filled in for team events, but a signature
from just one of the competitors on a team is enough.
A6. Make sure your contests follow the <Link href="/rules">rules</Link> (while the results on Cubing Contests
are considered unofficial, we still strive to ensure consistency and fairness). To generate the scorecards,
you can click <b className="hl">Edit</b> and click <b className="hl">Scorecards</b> (this button becomes
available after the contest gets approved). Since there is no registration through Cubing Contests yet, the
names won't be filled in and there are no groups, but there is one page for each round of each event. You can
print as many copies of each page as you need for the corresponding rounds, and ask the competitors to fill
their names in by hand when submitting their puzzles. Keep in mind that all names must be filled in for team
events, but a signature from just one of the competitors on a team is enough.
</p>
<p>
A7. To do data entry, click <b className="hl">Results</b> on the moderator dashboard, select the event and
Expand Down
168 changes: 168 additions & 0 deletions client/app/rules/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use client';

import { useEffect, useState } from 'react';
import myFetch from '~/helpers/myFetch';
import { IFeEvent } from '@sh/types';
import { INavigationItem } from '~/helpers/interfaces/NavigationItem';
import Tabs from '@c/UI/Tabs';
import ErrorMessages from '@c/UI/ErrorMessages';
import MarkdownDescription from '@c/MarkdownDescription';

const RulesPage = () => {
const [errorMessages, setErrorMessages] = useState<string[]>([]);
const [activeTab, setActiveTab] = useState('general');
const [events, setEvents] = useState<IFeEvent[]>([]);

const tabs: INavigationItem[] = [
{ title: 'General', value: 'general' },
{ title: 'Unofficial Competitions', shortTitle: 'Unofficial', value: 'unofficial' },
{ title: 'Meetups', value: 'meetups' },
];

useEffect(() => {
myFetch.get('/events/with-rules').then(({ payload, errors }) => {
if (errors) setErrorMessages(errors);
else setEvents(payload);
});
}, []);

return (
<div>
<h2 className="mb-4 text-center">Rules</h2>

<ErrorMessages errorMessages={errorMessages} />

<Tabs activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} />

{activeTab === 'general' && (
<>
<ol className="lh-lg">
<li>
The <a href="https://www.worldcubeassociation.org/regulations/full/">WCA Regulations</a> must be followed
wherever possible.
</li>
<li>
Judges and equipment (i.e. timers, stopwatches, sight blockers, etc.) are required for unofficial events
at WCA competitions and for unofficial competitions.
</li>
<li>Gen 2 timers are allowed in addition to the timers allowed for WCA competitions.</li>
<li>
<a href="https://experiments.cubing.net/cubing.js/mark3/">cubing.js</a> or{' '}
<a href="https://cstimer.net/">csTimer</a> scrambles must be used for twisty puzzle events. In particular,
random-state scrambles must be used for a puzzle, if available.
</li>
<li>Only organizers and Delegates of WCA competitions are allowed to hold unofficial events at them.</li>
</ol>

<h4 className="my-4">Relay events</h4>
<ul className="list-inline ps-3 lh-lg">
<li>R1. The judge uncovers all puzzles at once.</li>
<li>
R2. An attempt includes a normal 15-second inspection phase, regardless of the number of puzzles. The
competitor(s) is free to inspect any of the puzzles in any order during this phase.
</li>
</ul>

<h4 className="my-4">Team events</h4>
<ul className="list-inline ps-3 lh-lg">
<li>T1. There must be no physical contact between any members of a team during an attempt.</li>
<li>T2. All members of a team may communicate with each other and with the judge.</li>
</ul>

{/*
<h4 className="my-4">Fully blindfolded events</h4>
<p>
A fully blindfolded phase proceeds like a normal blindfolded solve (see{' '}
<a href="https://www.worldcubeassociation.org/regulations/#article-B-blindfolded">
Article B of the WCA Regulations
</a>
), with the following changes:
</p>
<ul className="list-inline ps-3 lh-lg">
<li>F1. The competitor must be blindfolded before starting the attempt.</li>
<li>
F2. The puzzle must be placed (or remain placed) on the mat uncovered after the competitor is blindfolded
but before the start of the attempt.
</li>
<li>
F3. The competitor begins the fully blindfolded timing phase by starting a Stackmat timer (similar to B1).
</li>
<li>
F4. The judge should place the sight blocker in front of the competitor before the timer starts. While
this is the responsibility of the judge, the competitor is encouraged to signal or briefly communicate
with the judge to ensure the sight blocker is placed ahead of time (e.g. saying "I'm about to start the
timer" out loud during a 3x3x3 Speed-Blind attempt shortly before donning the blindfold).
</li>
<li>
F5. The memorization phase (B2) is not included. Starting the timer immediately starts the blindfolded
phase (B3).
</li>
</ul>
*/}
</>
)}
{activeTab === 'unofficial' && (
<>
<p>
These rules only apply to unofficial competitions, and they supplement the general rules, with some points
being overridden.
</p>
<ul className="list-inline ps-3 lh-lg">
<li>
U1. An unofficial competition may not have fewer than three competitors in total. Such competitions will
be removed without the results being published.
</li>
<li>U2. An unofficial competition may not be held at a private residence.</li>
</ul>
</>
)}
{activeTab === 'meetups' && (
<>
<p>
These rules only apply to meetups, and they supplement the general rules, with some points being overridden.
</p>
<ul className="list-inline ps-3 lh-lg">
<li>
M1. Timers, stopwatches and other official equipment is not required. Mobile devices may be used to time
attempts.
</li>
<li className="ps-3">M1.1. Inspection time must still be followed.</li>
<li>M2. Judges are not required.</li>
<li>
M3. A meetup may not have fewer than three competitors in total. Such meetups will be removed without the
results being published.
</li>
<li>M4. A meetup may not be held at a private residence.</li>
</ul>
</>
)}

{events.length > 0 && (
<>
<hr />
<h3>Event rules</h3>
<p>
These rules apply to each event individually. If an event is not listed here, it must follow the most
relevant WCA Regulations, based on the nature of the event.
</p>
{events.map((event) => (
<div key={event.eventId}>
<h4 className="my-3">{event.name}</h4>
<MarkdownDescription>{event.ruleText}</MarkdownDescription>
</div>
))}
</>
)}

<hr />
<h3>License</h3>
<p>
The contents of this page are available under the{' '}
<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC Attribution-ShareAlike 4.0 International</a>{' '}
license.
</p>
</div>
);
};

export default RulesPage;
3 changes: 3 additions & 0 deletions server/src/modules/email/templates/contest-submitted.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<p>
### [Event Details]({{contestUrl}}/events)
</p>
<p>
### [Rules]({{ccUrl}}/rules)
</p>
</div>
{{/if}}
</body>
Expand Down
6 changes: 6 additions & 0 deletions server/src/modules/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ export class EventsController {

return await this.eventsService.updateEvent(eventId, updateEventDto);
}

// GET /events/with-rules
@Get('with-rules')
async getEventsWithRules() {
return await this.eventsService.getEventsWithRules();
}
}
Loading

0 comments on commit 8b3a56a

Please sign in to comment.