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 && +
    +
    +
    +
    + {t("Name")} +
    + setName(e.target.value)} + className="input input-bordered input-primary w-full"/> +
    +
    +
    + {t("Email")} +
    + updateEmail(e.target)} + className={"input input-bordered input-primary w-full" + (emailValid ? "" : " input-error")}/> + {!emailValid && +
    + {t("Enter a valid email address")} +
    + } +
    +
    +
    + {t("Password")} +
    + setPassword(e.target.value)} + className="input input-bordered input-primary w-full"/> +
    +
    +
    + {t("Repeat password")} +
    + setRepeatPassword(e.target.value)} + className="input input-bordered input-primary w-full"/> + {repeatPassword && password != repeatPassword && +
    + {t("Passwords must be the same")} +
    + } +
    +
    + +
    +
    +
    + } +
    +
    + + ); +} \ 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,