diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 67d0d2c..79f8ff5 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -60,6 +60,9 @@ jobs:
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=semver,pattern={{version}}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
diff --git a/public/i18n/da/translation.json b/public/i18n/da/translation.json
index 40291d3..9250522 100644
--- a/public/i18n/da/translation.json
+++ b/public/i18n/da/translation.json
@@ -2,6 +2,7 @@
"Add JID Code": "Tilføj JID kode",
"Can't add your own location": "Kan ikke tilføje din egen lokation",
"Checking JID Code": "Checker JID kode",
+ "Email": "Email",
"Enter JID Code": "Indtast JID kode",
"Fewer JID codes than last year": "↘\uFE0E {{difference}} færre JID koder ({{percentage}}%)",
"Fewer unique JID codes than last year": "↘\uFE0E {{difference}} færre unikke JID koder ({{percentage}}%)",
@@ -20,7 +21,10 @@
"Name": "Navn",
"No country with the code": "Intet land med landekoden '{{code}}'",
"Participants": "Deltagere",
+ "Password": "Kodeord",
"Pin code": "Pinkode",
+ "Register": "Registrer",
+ "Repeat password": "Gentag kodeord",
"Same as last year": "Samme som sidste år",
"Show": "Vis",
"Total JID codes found": "Total antal JID kode",
diff --git a/public/i18n/en/translation.json b/public/i18n/en/translation.json
index 396e0ac..b82ed5b 100644
--- a/public/i18n/en/translation.json
+++ b/public/i18n/en/translation.json
@@ -2,6 +2,7 @@
"Add JID Code": "Add JID Code",
"Can't add your own location": "Can't add your own location",
"Checking JID Code": "Checking JID Code",
+ "Email": "Email",
"Enter JID Code": "Enter JID Code",
"Fewer JID codes than last year": "↘\uFE0E {{difference}} fewer JID codes ({{percentage}}%)",
"Fewer unique JID codes than last year": "↘\uFE0E {{difference}} fewer unique JID codes ({{percentage}}%)",
@@ -20,7 +21,10 @@
"Name": "Name",
"No country with the code": "No country with the code '{{code}}'",
"Participants": "Participants",
+ "Password": "Password",
"Pin code": "Pin code",
+ "Register": "Register",
+ "Repeat password": "Repeat password",
"Same as last year": "Same as last year",
"Show": "Show",
"Total JID codes found": "Total JID codes found",
diff --git a/src/admin/AdminTopNav.tsx b/src/admin/AdminTopNav.tsx
index 17b8364..bf72633 100644
--- a/src/admin/AdminTopNav.tsx
+++ b/src/admin/AdminTopNav.tsx
@@ -49,6 +49,8 @@ export default function AdminTopNav(props: {
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 drop-shadow-md bg-primary rounded-box w-52 text-primary-content font-bold">
{!adminTopNavFragment.authenticatedAdmin?.id &&
{t("Login")}}
+ {!adminTopNavFragment.authenticatedAdmin?.id &&
+ {t("Register")}}
{adminTopNavFragment.authenticatedAdmin?.id &&
{t("Locations")}}
{adminTopNavFragment.authenticatedAdmin?.id &&
diff --git a/src/admin/register/AdminRegister.tsx b/src/admin/register/AdminRegister.tsx
new file mode 100644
index 0000000..a85f5dd
--- /dev/null
+++ b/src/admin/register/AdminRegister.tsx
@@ -0,0 +1,125 @@
+import {useMutation, useSuspenseQuery} from "@apollo/client";
+
+import AdminTopNav from "../AdminTopNav.tsx";
+import {AdminRegisterQuery, RegisterAdminMutation} from "./AdminRegisterQueries.tsx";
+import {Form, Link} from "react-router-dom";
+import {useState} from "react";
+import {useTranslation} from "react-i18next";
+
+export default function AdminRegister() {
+ const {t} = useTranslation()
+
+ const {data, refetch} = useSuspenseQuery(AdminRegisterQuery);
+ const [registerAdminMutation] = useMutation(RegisterAdminMutation)
+
+ const [registered, setRegistered] = useState(false);
+
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [emailValid, setEmailValid] = useState(true);
+ const [password, setPassword] = useState("");
+ const [repeatPassword, setRepeatPassword] = useState("");
+
+ const updateEmail = (field: HTMLInputElement) => {
+ setEmail(field.value)
+ setEmailValid(field.value ? field.validity.valid : true)
+ }
+
+ const formValid = name && email && password && password === repeatPassword;
+
+ const register = async () => {
+ if (formValid) {
+ const {data: mutationResult} = await registerAdminMutation({
+ variables: {
+ name,
+ email,
+ password,
+ }
+ })
+
+ if (mutationResult?.createAdmin?.id) {
+ setRegistered(true);
+ }
+ }
+ };
+
+ return (
+ <>
+
+
+
+ {registered &&
+
+
+ {t("Registration successful.")}
+
+
+ {t("Go to login")}
+
+
+ }
+
+ {!registered &&
+
+ }
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/admin/register/AdminRegisterQueries.tsx b/src/admin/register/AdminRegisterQueries.tsx
new file mode 100644
index 0000000..3611e05
--- /dev/null
+++ b/src/admin/register/AdminRegisterQueries.tsx
@@ -0,0 +1,15 @@
+import {gql} from "../../graphql";
+
+export const AdminRegisterQuery = gql(/* GraphQL */ `
+ query AdminRegisterQuery {
+ ...AdminTopNavFragment
+ }
+`)
+
+export const RegisterAdminMutation = gql(/* GraphQL */ `
+ mutation RegisterAdminMutation($name: String!, $email: String!, $password: String!) {
+ createAdmin(input: {name: $name, email: $email, password: $password}) {
+ id
+ }
+ }
+`);
diff --git a/src/graphql/__autogenerated/gql.ts b/src/graphql/__autogenerated/gql.ts
index 308191e..cfaec0b 100644
--- a/src/graphql/__autogenerated/gql.ts
+++ b/src/graphql/__autogenerated/gql.ts
@@ -22,6 +22,8 @@ const documents = {
"\n query AdminLoginQuery {\n authenticatedAdmin {\n id\n }\n ...AdminTopNavFragment\n }\n": types.AdminLoginQueryDocument,
"\n mutation AuthenticateAdminMutation($email: String!, $password: String!) {\n authenticateAdmin(email: $email, password: $password)\n }\n": types.AuthenticateAdminMutationDocument,
"\n query AdminOverviewQuery {\n authenticatedAdmin {\n id\n }\n locations {\n id\n name\n code {\n value\n }\n year\n participants {\n id\n }\n jidCodeStats {\n count\n uniqueCount\n uniqueCountryCount\n }\n }\n ...AdminTopNavFragment\n }\n": types.AdminOverviewQueryDocument,
+ "\n query AdminRegisterQuery {\n ...AdminTopNavFragment\n }\n": types.AdminRegisterQueryDocument,
+ "\n mutation RegisterAdminMutation($name: String!, $email: String!, $password: String!) {\n createAdmin(input: {name: $name, email: $email, password: $password}) {\n id\n }\n }\n": types.RegisterAdminMutationDocument,
"\n query ServerVersionQuery {\n serverVersion\n }\n": types.ServerVersionQueryDocument,
"\n fragment MapOverview on JidCodeStats {\n countryStats {\n country\n uniqueCount\n }\n }\n": types.MapOverviewFragmentDoc,
"\n query VerifyLocationCode($locationCode: String!, $noLocationCode: Boolean!) {\n locationByCode(code: $locationCode) @skip(if: $noLocationCode) {\n id\n }\n ...AdminTopNavFragment\n }\n": types.VerifyLocationCodeDocument,
@@ -88,6 +90,14 @@ export function gql(source: "\n mutation AuthenticateAdminMutation($email: St
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\n query AdminOverviewQuery {\n authenticatedAdmin {\n id\n }\n locations {\n id\n name\n code {\n value\n }\n year\n participants {\n id\n }\n jidCodeStats {\n count\n uniqueCount\n uniqueCountryCount\n }\n }\n ...AdminTopNavFragment\n }\n"): (typeof documents)["\n query AdminOverviewQuery {\n authenticatedAdmin {\n id\n }\n locations {\n id\n name\n code {\n value\n }\n year\n participants {\n id\n }\n jidCodeStats {\n count\n uniqueCount\n uniqueCountryCount\n }\n }\n ...AdminTopNavFragment\n }\n"];
+/**
+ * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function gql(source: "\n query AdminRegisterQuery {\n ...AdminTopNavFragment\n }\n"): (typeof documents)["\n query AdminRegisterQuery {\n ...AdminTopNavFragment\n }\n"];
+/**
+ * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function gql(source: "\n mutation RegisterAdminMutation($name: String!, $email: String!, $password: String!) {\n createAdmin(input: {name: $name, email: $email, password: $password}) {\n id\n }\n }\n"): (typeof documents)["\n mutation RegisterAdminMutation($name: String!, $email: String!, $password: String!) {\n createAdmin(input: {name: $name, email: $email, password: $password}) {\n id\n }\n }\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/src/graphql/__autogenerated/graphql.ts b/src/graphql/__autogenerated/graphql.ts
index 297ba51..485a6b2 100644
--- a/src/graphql/__autogenerated/graphql.ts
+++ b/src/graphql/__autogenerated/graphql.ts
@@ -284,6 +284,23 @@ export type AdminOverviewQueryQuery = (
& { ' $fragmentRefs'?: { 'AdminTopNavFragmentFragment': AdminTopNavFragmentFragment } }
);
+export type AdminRegisterQueryQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type AdminRegisterQueryQuery = (
+ { __typename?: 'Query' }
+ & { ' $fragmentRefs'?: { 'AdminTopNavFragmentFragment': AdminTopNavFragmentFragment } }
+);
+
+export type RegisterAdminMutationMutationVariables = Exact<{
+ name: Scalars['String']['input'];
+ email: Scalars['String']['input'];
+ password: Scalars['String']['input'];
+}>;
+
+
+export type RegisterAdminMutationMutation = { __typename?: 'Mutation', createAdmin?: { __typename?: 'Admin', id: string } | null };
+
export type ServerVersionQueryQueryVariables = Exact<{ [key: string]: never; }>;
@@ -387,6 +404,8 @@ export const AdminLocationParticipantSubscriptionDocument = {"kind":"Document","
export const AdminLoginQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminLoginQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminTopNavFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminTopNavFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
export const AuthenticateAdminMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AuthenticateAdminMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticateAdmin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}]}}]} as unknown as DocumentNode;
export const AdminOverviewQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOverviewQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"locations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"year"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"jidCodeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"uniqueCount"}},{"kind":"Field","name":{"kind":"Name","value":"uniqueCountryCount"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminTopNavFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminTopNavFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
+export const AdminRegisterQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminRegisterQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminTopNavFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminTopNavFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
+export const RegisterAdminMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RegisterAdminMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAdmin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode;
export const ServerVersionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerVersionQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverVersion"}}]}}]} as unknown as DocumentNode;
export const VerifyLocationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyLocationCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locationCode"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"noLocationCode"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationByCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"code"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationCode"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"skip"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"if"},"value":{"kind":"Variable","name":{"kind":"Name","value":"noLocationCode"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminTopNavFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminTopNavFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedAdmin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
export const CreateParticipantMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateParticipantMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateParticipantInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createParticipant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"pinCode"}}]}}]}}]} as unknown as DocumentNode;
diff --git a/src/router.tsx b/src/router.tsx
index b40fcad..0d9a48d 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -7,6 +7,7 @@ import AdminOverview from "./admin/overview/AdminOverview.tsx";
import Logout from "./Logout.tsx";
import ErrorPage from "./ErrorPage.tsx";
import AdminLocation from "./admin/location/AdminLocation.tsx";
+import AdminRegister from "./admin/register/AdminRegister.tsx";
export const router = createBrowserRouter([{
path: "/",
@@ -15,6 +16,9 @@ export const router = createBrowserRouter([{
path: "/admin",
element: ,
children: [{
+ path: "register",
+ element:
+ }, {
path: "login",
element: ,
action: AdminLoginAction
@@ -23,11 +27,11 @@ export const router = createBrowserRouter([{
element:
}, {
path: ":locationCode",
- element: ,
+ element: ,
errorElement:
}, {
path: ":locationCode/:year",
- element: ,
+ element: ,
errorElement:
}, {
index: true,