Compare commits

..

5 Commits

8 changed files with 492 additions and 23 deletions

237
package-lock.json generated
View File

@ -9,9 +9,11 @@
"version": "0.0.1",
"dependencies": {
"@floating-ui/dom": "^1.6.12",
"pocketbase": "^0.22.1"
"pocketbase": "^0.22.1",
"uuid": "^11.0.3"
},
"devDependencies": {
"@fsouza/prettierd": "^0.25.4",
"@skeletonlabs/skeleton": "^2.10.3",
"@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-auto": "^3.0.0",
@ -19,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@types/node": "^22.10.2",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
@ -58,6 +61,60 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/types": "^7.26.3"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
@ -491,6 +548,24 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@fsouza/prettierd": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@fsouza/prettierd/-/prettierd-0.25.4.tgz",
"integrity": "sha512-y67VtstV11r2lD2ZF8owCPRgUXO4JjrBSAmqehzaaBBSXXzWtLEt3s6VQiHXXSgw0X2BBPUnyy+L2O21S4uiDA==",
"dev": true,
"license": "ISC",
"dependencies": {
"core_d": "^6.1.0",
"prettier": "^3.3.3"
},
"bin": {
"prettierd": "bin/prettierd"
},
"optionalDependencies": {
"@babel/parser": "^7.26.2",
"@typescript-eslint/typescript-estree": "^8.14.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1079,6 +1154,75 @@
"optional": true,
"peer": true
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/types": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz",
"integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
"integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@typescript-eslint/types": "8.18.0",
"@typescript-eslint/visitor-keys": "8.18.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz",
"integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@typescript-eslint/types": "8.18.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@ -1424,6 +1568,16 @@
"node": ">= 0.6"
}
},
"node_modules/core_d": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/core_d/-/core_d-6.1.0.tgz",
"integrity": "sha512-vYgenhJ8CYCj+7LPbPdyFo2u0Doavfbi/vhFpR/BsW9/iUAhuKd+sw2l4CHXhaXIo4/058p2nlsAtbL7iswm5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"supports-color": "^8.1.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -1616,6 +1770,20 @@
"node": ">=6"
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esm-env": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
@ -1806,6 +1974,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -3240,6 +3418,20 @@
"node": ">=6"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@ -3435,6 +3627,22 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@ -3656,6 +3864,20 @@
"node": ">=6"
}
},
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=16"
},
"peerDependencies": {
"typescript": ">=4.2.0"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -3738,6 +3960,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz",

View File

@ -10,6 +10,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@fsouza/prettierd": "^0.25.4",
"@skeletonlabs/skeleton": "^2.10.3",
"@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-auto": "^3.0.0",
@ -17,6 +18,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@types/node": "^22.10.2",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
@ -30,6 +32,7 @@
},
"dependencies": {
"@floating-ui/dom": "^1.6.12",
"pocketbase": "^0.22.1"
"pocketbase": "^0.22.1",
"uuid": "^11.0.3"
}
}

View File

@ -0,0 +1,86 @@
<script lang="ts">
import { ListBox, ListBoxItem, popup, type PopupSettings } from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
import type { HTMLInputAttributes } from "svelte/elements";
import { v4 as uuid } from "uuid";
import UserIcon from "./svg/UserIcon.svelte";
export interface DropdownOption {
label: string;
value: string;
}
interface SearchProps extends HTMLInputAttributes {
children: Snippet;
/** Placeholder for the empty input element */
placeholder?: string;
/** Form name of the input element, to reference input data after form submission */
name?: string;
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
input_variable: string;
/** The ID of the popup to trigger. UUID by default. */
popup_id?: string;
/** The [PopupSettings] object for the popup to trigger. */
popup_settings?: PopupSettings;
/** The options this autocomplete component allows to choose from.
* Example: [[{ label: "Aston", value: "0" }, { label: "VCARB", value: "1" }]].
*/
options: DropdownOption[];
}
let {
children,
placeholder = "",
name = "",
labelwidth = "auto",
input_variable,
popup_id = uuid(),
popup_settings = {
event: "click",
target: popup_id,
placement: "bottom",
closeQuery: ".listbox-item",
},
options,
...restProps
}: SearchProps = $props();
const get_label = (value: string): string | undefined => {
return options.find((o) => o.value === value)?.label;
};
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div
class="input-group-shim select-none text-nowrap text-neutral-900"
style="width: {labelwidth};"
>
{@render children()}
</div>
<input
use:popup={popup_settings}
type="text"
value={get_label(input_variable) ?? placeholder}
{...restProps}
/>
</div>
<div data-popup={popup_id} class="card z-10 w-auto p-2 shadow">
<ListBox>
{#each options as option}
<ListBoxItem bind:group={input_variable} {name} value={option.value}
>{option.label}</ListBoxItem
>
{/each}
</ListBox>
<div class="bg-surface-100-800-token arrow"></div>
</div>

View File

@ -0,0 +1,83 @@
<script lang="ts">
import {
Autocomplete,
popup,
type AutocompleteOption,
type PopupSettings,
} from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
import { v4 as uuid } from "uuid";
interface SearchProps {
children: Snippet;
/** Placeholder for the empty input element */
placeholder?: string;
/** Form name of the input element, to reference input data after form submission */
name?: string;
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
input_variable: string;
/** The ID of the input element. UUID by default. */
input_id?: string;
/** The ID of the popup to trigger. UUID by default. */
popup_id?: string;
/** The [PopupSettings] object for the popup to trigger. */
popup_settings?: PopupSettings;
/** The event handler updating the [input_variable] after selection. */
selection_handler?: (event: CustomEvent<AutocompleteOption<string>>) => void;
/** The options this autocomplete component allows to choose from.
* Example: [[{ label: "Aston", value: "0" }, { label: "VCARB", value: "1" }]].
*/
options: AutocompleteOption<string, unknown>[];
}
let {
children,
placeholder = "",
name = "",
labelwidth = "auto",
input_variable,
input_id = uuid(),
popup_id = uuid(),
popup_settings = {
event: "focus-click",
target: popup_id,
placement: "bottom",
},
selection_handler = (event: CustomEvent<AutocompleteOption<string>>): void => {
input_variable = event.detail.label;
},
options,
}: SearchProps = $props();
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div
class="input-group-shim select-none text-nowrap text-neutral-900"
style="width: {labelwidth};"
>
{@render children()}
</div>
<input
id={input_id}
type="search"
{placeholder}
{name}
bind:value={input_variable}
use:popup={popup_settings}
/>
</div>
<div data-popup={popup_id} class="card z-10 w-auto p-2 shadow" tabindex="-1">
<Autocomplete bind:input={input_variable} {options} on:selection={selection_handler} />
</div>

View File

@ -1,9 +1,22 @@
import Input from "./Input.svelte";
import Button from "./Button.svelte";
import Card from "./Card.svelte";
import Search from "./Search.svelte";
import Dropdown from "./Dropdown.svelte";
// import type DropdownOption from "./Dropdown.svelte";
import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte";
import UserIcon from "./svg/UserIcon.svelte";
import PasswordIcon from "./svg/PasswordIcon.svelte";
export { Input, Button, Card, MenuDrawerIcon, UserIcon, PasswordIcon };
export {
Input,
Button,
Card,
Search,
Dropdown,
// type DropdownOption,
MenuDrawerIcon,
UserIcon,
PasswordIcon,
};

View File

@ -1,7 +1,7 @@
/**
* Retrieve an arbitrary object with a matching ID from an Array.
* Supposed to use on collections returned by the PocketBase API.
* Supposed to be used on collections returned by the PocketBase API.
*/
export const get_by_id = <T extends object>(objects: Array<T>, id: string): T | undefined => {
export const get_by_id = <T extends object>(objects: T[], id: string): T | undefined => {
return objects.find((o: T) => ("id" in o ? o.id === id : false));
};

View File

@ -38,12 +38,37 @@ export const actions = {
},
create_driver: async ({ request, locals }) => {
if (!locals.admin) return { unauthorized: true };
const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["firstname", "lastname", "code", "team", "headshot", "active"]);
console.log(data);
const record: Driver = await locals.pb.collection("drivers").create(data);
return { tab: 1 };
},
update_driver: async ({ request, locals }) => {
if (!locals.admin) return { unauthorized: true };
const data: FormData = form_data_clean(await request.formData());
const id: string = form_data_get_and_remove_id(data);
const record: Driver = await locals.pb.collection("drivers").update(id, data);
return { tab: 1 };
},
delete_driver: async ({ request, locals }) => {
if (!locals.admin) return { unauthorized: true };
const data: FormData = form_data_clean(await request.formData());
const id: string = form_data_get_and_remove_id(data);
await locals.pb.collection("drivers").delete(id);
return { tab: 1 };
},
@ -70,8 +95,8 @@ export const actions = {
// This "load" function runs serverside only, as it's located inside +page.server.ts
export const load: PageServerLoad = async ({ fetch, locals }) => {
const fetch_teams = async (): Promise<Array<Team>> => {
const teams: Array<Team> = await locals.pb.collection("teams").getFullList({
const fetch_teams = async (): Promise<Team[]> => {
const teams: Team[] = await locals.pb.collection("teams").getFullList({
sort: "+name",
fetch: fetch,
});
@ -83,8 +108,8 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
return teams;
};
const fetch_drivers = async (): Promise<Array<Driver>> => {
const drivers: Array<Driver> = await locals.pb.collection("drivers").getFullList({
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+lastname",
fetch: fetch,
});
@ -96,11 +121,11 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
return drivers;
};
const fetch_races = async (): Promise<Array<Race>> => {
const fetch_races = async (): Promise<Race[]> => {
return [];
};
const fetch_substitutions = async (): Promise<Array<Substitution>> => {
const fetch_substitutions = async (): Promise<Substitution[]> => {
return [];
};

View File

@ -1,11 +1,13 @@
<script lang="ts">
import { Input, Button, Card } from "$lib/components";
import { Input, Button, Card, Search, Dropdown } from "$lib/components";
import { get_image_preview_event_handler } from "$lib/image";
import { get_by_id } from "$lib/database";
import type { Team } from "$lib/schema";
import type { Driver, Team } from "$lib/schema";
import { type PageData, type ActionData } from "./$types";
import { FileDropzone, Tab, TabGroup } from "@skeletonlabs/skeleton";
import { FileDropzone, Tab, TabGroup, type AutocompleteOption } from "@skeletonlabs/skeleton";
// TODO: Why does this work but import { type DropdownOption } from "$lib/components" does not?
import type { DropdownOption } from "$lib/components/Dropdown.svelte";
let { data, form }: { data: PageData; form: ActionData } = $props();
@ -14,6 +16,17 @@
// console.log(`Form returned current_tab=${form.current_tab}`);
current_tab = form.tab;
}
// Maps driver to team: <driver.id, team.id>
let create_driver_team_select_value: string = $state("");
let update_driver_team_select_values: { [key: string]: string } = $state({});
data.drivers.forEach((driver: Driver) => {
update_driver_team_select_values[driver.id] = driver.team;
});
const driver_team_select_options: DropdownOption[] = data.teams.map((team: Team) => {
return { label: team.name, value: team.id } as DropdownOption;
});
</script>
<svelte:head>
@ -121,8 +134,6 @@
<!-- Drivers Tab -->
<!-- Drivers Tab -->
<!-- TODO: Team select -->
<!-- TODO: Active switch -->
<div class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">
<!-- List all drivers inside the database -->
{#each data.drivers as driver}
@ -155,9 +166,14 @@
labelwidth="120px"
disabled={!data.admin}>Driver Code</Input
>
<Button>
<img src={get_by_id<Team>(data.teams, driver.team)?.logo_url} alt="" />
</Button>
<!-- Driver team input -->
<Dropdown
name="team"
input_variable={update_driver_team_select_values[driver.id]}
labelwidth="120px"
options={driver_team_select_options}>Team</Dropdown
>
<!-- Headshot upload -->
<FileDropzone
@ -201,7 +217,6 @@
<Input
id="driver_first_name_create"
name="firstname"
placeholder="First Name"
labelwidth="120px"
disabled={!data.admin}
required>First Name</Input
@ -209,7 +224,6 @@
<Input
id="driver_last_name_create"
name="lastname"
placeholder="Last Name"
labelwidth="120px"
disabled={!data.admin}
required>Last Name</Input
@ -217,12 +231,22 @@
<Input
id="driver_code_create"
name="code"
placeholder="Driver Code"
labelwidth="120px"
disabled={!data.admin}
maxlength={3}
minlength={3}
required>Driver Code</Input
>
<!-- Driver team input -->
<Dropdown
input_variable={create_driver_team_select_value}
name="team"
labelwidth="120px"
options={driver_team_select_options}
required>Team</Dropdown
>
<!-- Headshot upload -->
<FileDropzone
name="headshot"