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

[WIP] Manage sessions; #12

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
106 changes: 103 additions & 3 deletions src/routes/sessions/new/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,123 @@
// YOLO: on

import { FunctionalComponent, h } from 'preact';
import { FunctionalComponent, h, JSX } from 'preact';
import { useEffect, useState } from 'preact/hooks';

import { fetchFullSet } from '../../../services/set';
import { createSession } from '../../../services/session';
import Set from '../../../components/sets/Set';
import type { NewSessionStateType, FullSetType } from '../../../types/types';

import style from './style.css';

type NewSessionPropsType = {
setId?: string,
};

const NewSession: FunctionalComponent = () => {
const NewSession: FunctionalComponent<NewSessionPropsType> = (props) => {
const _now = new Date();
const [set, setSet] = useState<FullSetType | null>(null);
const [session, setSession] = useState<NewSessionStateType>({
setId: props.setId || '',
name: { value: '', valid: false, message: 'Name can not be empty' },
alias: { value: '', valid: false, message: 'Alias can not be empty' },
date: { value: _now, internal: _now.toISOString(), valid: true, message: '' },
});

useEffect(() => {
if (!session.setId) {
setSet(null);
return;
}

fetchFullSet(session.setId).then((puzzleSet) => {
setSet(puzzleSet);
}, (error) => {
alert(JSON.stringify(error));
})
}, [session.setId]);

function onNameChange(event: JSX.TargetedEvent<HTMLInputElement, Event>): void {
const { value } = event.currentTarget;

if (!value) {
return setSession({ ...session, name: { value, valid: false, message: 'Name can not be empty' } });
}

return setSession({ ...session, name: { value, valid: true, message: '' } });
}

function onAliasChange(event: JSX.TargetedEvent<HTMLInputElement, Event>): void {
const { value } = event.currentTarget;

if (!value) {
return setSession({ ...session, alias: { value, valid: false, message: 'Alias can not be empty' } });
}

if (!/^[A-Za-z0-9_-]+$/.test(value)) {
return setSession({ ...session, alias: { value, valid: false, message: 'Alias can contain only letters, numbers, dash and underscore' } });
}

return setSession({ ...session, alias: { value, valid: true, message: '' } });
}

function onDateChange(event: JSX.TargetedEvent<HTMLInputElement, Event>): void {
const { validity, valueAsDate } = event.currentTarget;

if (!validity.valid || !valueAsDate) {
return setSession({ ...session, date: { value: new Date(), valid: false, message: 'Invalid date' } });
}

return setSession({ ...session, date: { value: valueAsDate, internal: valueAsDate.toISOString(), valid: true, message: '' } });
}

async function createSessionClick() {
try {
const alias = await createSession(session);
alert(`Session created! Session alias = ${alias}`);
} catch(error) {
alert(JSON.stringify(error));
}
}

return (
<>
<div class={style.title}>
NEW SESSION
</div>
<div class={style.actions}>
<button className="-positive -bigger" onClick={() => {}}>Create</button>
<button className="-positive -bigger" onClick={createSessionClick}>Create</button>
</div>
<div class={style.inputs}>
<div class={style.setId}>
<div class={style.propName}>Set id:</div>
<input value={session.setId} onInput={(event) => setSession({ ...session, setId: event.currentTarget.value })} class={style.setIdInput} />
</div>
<div class={style.name}>
<div class={style.propName}>Name:</div>
<input class={session?.name?.valid ? style.inputValid : style.inputInvalid} value={session?.name?.value || ''} onInput={onNameChange} />
{session?.name?.message && <div class={style.errorMessage}>{session?.name?.message}</div>}
</div>
<div class={style.alias}>
<div class={style.propName}>Alias:</div>
<input class={session?.alias?.valid ? style.inputValid : style.inputInvalid} value={session?.alias?.value || ''} onInput={onAliasChange} />
{session?.alias?.message && <div class={style.errorMessage}>{session?.alias?.message}</div>}
</div>
<div class={style.alias}>
<div class={style.propName}>Date:</div>
<input
type="date"
min={new Intl.DateTimeFormat('en-CA', {}).format(new Date())}
class={session?.date?.valid ? style.inputValid : style.inputInvalid}
value={new Intl.DateTimeFormat('en-CA', {}).format(session?.date?.value)}
onInput={onDateChange}
/>
{session?.date?.message && <div class={style.errorMessage}>{session?.date?.message}</div>}
</div>
</div>
<div class={style.setContainer}>
<div class={style.setContainerHeading}>Puzzle set:</div>
<Set set={set} collapsed={true} />
</div>
</>
);
Expand Down
29 changes: 28 additions & 1 deletion src/routes/sessions/new/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,32 @@
}

.inputs {
padding: .5em 0;
padding: .5em;
}
.inputValid {
border-color: #177317;
}
.inputInvalid {
border-color: #9b1d1d;
background: #fde4e4;
}
.errorMessage {
color: #9b1d1d;
font-size: 80%;
}
.propName {
font-weight: bold;
}
.setId {
display: flex;
}
.name {
display: flex;
}
.alias {
display: flex;
}

.setContainerHeading {
font-size: 1.2rem;
}
26 changes: 26 additions & 0 deletions src/services/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { NewSessionStateType } from '../types/types';

// const API_BASE_URL = `${window.location.origin}/api/`;
const API_BASE_URL = 'http://localhost:3000/api/';

export async function createSession(newSession: NewSessionStateType): Promise<string> {
const url = new URL('v1/createPuzzleSession', API_BASE_URL);

const response = await fetch(url.toString(), {
method: 'POST',
body: JSON.stringify({
name: newSession.name.value,
puzzleSetId: newSession.setId,
alias: newSession.alias.value,
date: newSession.date.internal, // ISO string
}),
headers: { 'Content-Type': 'application/json' },
});

if (!response.ok) {
throw await response.json();
}

const { puzzleSession } = await response.json();
return puzzleSession.alias;
};
24 changes: 16 additions & 8 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
export type GameType = 'CSS' | 'JS' | 'Lodash';
export type GameTypeType = 'cssqd' | 'jsqd' | '_qd';

type NewPuzzleStatePropertyType<ValueT, InternalT> = {
type ValidityStatePropertyType<ValueT, InternalT> = {
value: ValueT,
internal?: InternalT | null,
valid: boolean,
message?: string,
}

export type NewPuzzleStateType = {
name: NewPuzzleStatePropertyType<string, void>,
description: NewPuzzleStatePropertyType<string, void>,
timeLimit: NewPuzzleStatePropertyType<number, void>,
name: ValidityStatePropertyType<string, void>,
description: ValidityStatePropertyType<string, void>,
timeLimit: ValidityStatePropertyType<number, void>,
expected: string,
banned: NewPuzzleStatePropertyType<string, void>,
input: NewPuzzleStatePropertyType<string, string>,
solution: NewPuzzleStatePropertyType<string, void>,
solutionLengthLimit: NewPuzzleStatePropertyType<number, void>,
banned: ValidityStatePropertyType<string, void>,
input: ValidityStatePropertyType<string, string>,
solution: ValidityStatePropertyType<string, void>,
solutionLengthLimit: ValidityStatePropertyType<number, void>,
};

export type NewSetStateType = {
name: string,
order: FullPuzzleType[],
};

export type NewSessionStateType = {
setId: string,
name: ValidityStatePropertyType<string, void>,
alias: ValidityStatePropertyType<string, void>,
date: ValidityStatePropertyType<Date, string>,
participantLimit?: ValidityStatePropertyType<number, void>,
};

export type UserType = {
provider: string,
providerId: string,
Expand Down