Skip to content

Commit a548d43

Browse files
authored
feat: implement two modes register form (#35)
* feat: implement two modes register form * feat: add auto upload for competition * feat: upload file to scw bucket
1 parent 9a819fe commit a548d43

17 files changed

+4692
-1204
lines changed

.env.example

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@
44

55
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).
66
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
7-
8-
DATABASE_URL="postgres://xx:xxxxxxx@xxxxx/xxxxx"
7+
S3_ACCESS_KEY_ID=
8+
S3_ACCESS_KEY=
9+
S3_REGION=fr-par
10+
S3_ENDPOINT=
11+
S3_BUCKET_NAME=
12+
NEXT_PUBLIC_BUCKET_URL=

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ yarn-error.log*
4040

4141
# webstorm
4242
/.idea/
43+
.env*.local

app/admin-view/files.module.css

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.container {
2+
max-width: 800px;
3+
margin: 0 auto;
4+
padding: 2rem;
5+
text-align: center;
6+
}
7+
8+
.title {
9+
font-size: 2rem;
10+
margin-bottom: 2rem;
11+
}
12+
13+
.list {
14+
text-align: left;
15+
}
16+
17+
.details {
18+
margin-bottom: 1rem;
19+
border: 4px solid #ccc;
20+
border-radius: 8px;
21+
padding: 0.5rem 1rem;
22+
}
23+
24+
.summary {
25+
font-weight: bold;
26+
cursor: pointer;
27+
font-size: 1.1rem;
28+
}
29+
30+
.inner {
31+
padding-left: 1rem;
32+
margin-top: 0.5rem;
33+
}
34+
35+
.subdetails {
36+
margin: 0.5rem 0;
37+
color: #000;
38+
border: 1px solid #ddd;
39+
border-radius: 6px;
40+
padding: 0.75rem;
41+
background: #fff;
42+
}
43+
44+
.subsummary {
45+
font-weight: 500;
46+
cursor: pointer;
47+
font-size: 1rem;
48+
}
49+
50+
.ul {
51+
list-style: none;
52+
padding-left: 1rem;
53+
margin: 0.5rem 0 0;
54+
}
55+
56+
.li {
57+
margin: 0.25rem 0;
58+
padding: 0.5rem 0;
59+
}
60+
61+
.link {
62+
text-decoration: none;
63+
color: #0070f3;
64+
transition: color 0.2s ease;
65+
}
66+
67+
.link:hover {
68+
color: #0051a8;
69+
}
70+
71+
.center {
72+
text-align: center;
73+
padding: 2rem;
74+
}

app/admin-view/page.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import styles from './files.module.css';
5+
6+
interface Tree {
7+
[date: string]: {
8+
[name: string]: string[];
9+
};
10+
}
11+
12+
const BUCKET_URL = process.env.NEXT_PUBLIC_BUCKET_URL || '';
13+
14+
export default function FilesPage() {
15+
const [tree, setTree] = useState<Tree>({});
16+
const [loading, setLoading] = useState(true);
17+
18+
useEffect(() => {
19+
async function fetchFiles() {
20+
const res = await fetch('/save'); // route API
21+
const data = await res.json();
22+
setTree(data.tree || {});
23+
setLoading(false);
24+
}
25+
fetchFiles();
26+
}, []);
27+
28+
if (loading) {
29+
return <div className={styles.center}>Loading…</div>;
30+
}
31+
32+
return (
33+
<div className={styles.container}>
34+
<h1 className={styles.title}>Admin view</h1>
35+
<div className={styles.list}>
36+
{Object.keys(tree).map((date) => (
37+
<details key={date} className={styles.details}>
38+
<summary className={styles.summary}>{date}</summary>
39+
<div className={styles.inner}>
40+
{Object.keys(tree[date]).map((name) => (
41+
<details key={name} className={styles.subdetails}>
42+
<summary className={styles.subsummary}>{name}</summary>
43+
<ul className={styles.ul}>
44+
{tree[date][name].map((filename) => (
45+
<li key={filename} className={styles.li}>
46+
<a
47+
href={`${BUCKET_URL}/${date}/${name}/${filename}`}
48+
target="_blank"
49+
rel="noopener noreferrer"
50+
className={styles.link}
51+
>
52+
{filename}
53+
</a>
54+
</li>
55+
))}
56+
</ul>
57+
</details>
58+
))}
59+
</div>
60+
</details>
61+
))}
62+
</div>
63+
</div>
64+
);
65+
}

app/page.tsx

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { useForm } from 'react-hook-form';
44
import { useRouter } from 'next/navigation';
55
import { useEntryStore } from '../hooks/useEntryStore';
66
import React, { useEffect, useState } from 'react';
7-
import { TemplateName, templatesDictionary } from "../config/templates";
7+
import { TemplateName, templatesDictionary } from '../config/templates';
88
import Image from 'next/image';
99

1010
import styles from '../styles/register.module.scss';
11+
import { Tabs } from 'radix-ui';
12+
import slugify from 'slugify';
1113

1214
export default function Page() {
1315
const router = useRouter();
@@ -20,14 +22,20 @@ export default function Page() {
2022
const { register, handleSubmit, formState } = useForm({
2123
defaultValues: { fullName: entry?.fullName, templateName: '' },
2224
});
25+
const [error, setError] = useState<string | null>(null);
2326

2427
const onSubmit = async (data: { [x: string]: any }) => {
25-
updateIsLoading(true);
26-
updateFullName(data.fullName);
27-
updateTemplate(isTrainningSession ? selectedTemplate : data.templateName );
28-
29-
// TODO : Create user in DB
28+
setError(null);
29+
if(!isTrainningSession) {
30+
const isValidTemplate = Object.keys(templatesDictionary).includes(data.templateName)
31+
if(!isValidTemplate) {
32+
return setError('Invalid template name');
33+
}
34+
}
3035

36+
updateIsLoading(true);
37+
updateFullName(slugify(data.fullName, { lower: true, strict: true, trim: true }));
38+
updateTemplate(isTrainningSession ? selectedTemplate : data.templateName);
3139
updateId(0);
3240
updateIsLoading(false);
3341

@@ -52,49 +60,68 @@ export default function Page() {
5260
className={formState.errors.fullName ? styles.isWizz : ''}
5361
required
5462
/>
55-
<div className={styles.trainingToggle}>
56-
<input
57-
id='toggle-template-select'
58-
type='checkbox'
59-
checked={isTrainningSession}
60-
onChange={(e) => setIsTrainningSession(e.target.checked)}
61-
/>
62-
<label htmlFor='toggle-template-select'>Check for training session</label>
63-
</div>
63+
<h3>Select a mode</h3>
64+
<Tabs.Root className={styles.tabsRoot} defaultValue='training' onValueChange={value => setIsTrainningSession(value === 'training')}>
65+
<Tabs.List
66+
className={styles.tabsList}
67+
aria-label='Manage your account'
68+
>
69+
<Tabs.Trigger className={styles.tabsTrigger} value='training'>
70+
<h3>Training Mode</h3>
71+
</Tabs.Trigger>
72+
<Tabs.Trigger className={styles.tabsTrigger} value='competition'>
73+
<h3>Competition Mode</h3>
74+
</Tabs.Trigger>
75+
</Tabs.List>
76+
<Tabs.Content className={styles.tabsContent} value='training'>
77+
<fieldset>
78+
<h3>Select a template</h3>
79+
<select
80+
value={selectedTemplate}
81+
onChange={(e) =>
82+
setSelectedTemplate(e.target.value as TemplateName)
83+
}
84+
>
85+
{Object.keys(templatesDictionary)
86+
.filter(
87+
(name) =>
88+
!templatesDictionary[
89+
name as keyof typeof templatesDictionary
90+
].private
91+
)
92+
.map((name) => (
93+
<option key={name} value={name}>
94+
{name}
95+
</option>
96+
))}
97+
</select>
98+
<Image
99+
priority
100+
src={templatesDictionary[selectedTemplate].referenceImage}
101+
alt='Image template reference'
102+
width={200}
103+
height={200}
104+
/>
105+
</fieldset>
106+
</Tabs.Content>
107+
<Tabs.Content className={styles.tabsContent} value='competition'>
108+
<fieldset>
109+
<h3>Please set session code</h3>
110+
<input
111+
type='text'
112+
placeholder='Session Password'
113+
{...register('templateName', {
114+
required: !isTrainningSession,
115+
max: 80,
116+
})}
117+
className={formState.errors.fullName ? styles.isWizz : ''}
118+
required={isTrainningSession}
119+
/>
120+
{error && <span className={styles.error}>{error}</span>}
121+
</fieldset>
122+
</Tabs.Content>
123+
</Tabs.Root>
64124

65-
{isTrainningSession ? (
66-
<>
67-
<h3>Select a template</h3>
68-
<select
69-
value={selectedTemplate}
70-
onChange={(e) => setSelectedTemplate(e.target.value as TemplateName)}
71-
>
72-
{Object.keys(templatesDictionary).map((name) => (
73-
<option key={name} value={name}>
74-
{name}
75-
</option>
76-
))}
77-
</select>
78-
<Image
79-
priority
80-
src={templatesDictionary[selectedTemplate].referenceImage}
81-
alt='Image template reference'
82-
width={200}
83-
height={200}
84-
/>
85-
</>
86-
) : (
87-
<>
88-
<h3>Please set session code</h3>
89-
<input
90-
type='text'
91-
placeholder='Session Password'
92-
{...register('templateName', { required: !isTrainningSession, max: 80 })}
93-
className={formState.errors.fullName ? styles.isWizz : ''}
94-
required={!isTrainningSession}
95-
/>
96-
</>
97-
)}
98125
<input type='submit' className='button' />
99126
</form>
100127
</>

0 commit comments

Comments
 (0)