diff --git a/schema.yml b/schema.yml index a4cbc5bda..00fa11242 100644 --- a/schema.yml +++ b/schema.yml @@ -306,20 +306,63 @@ components: MemberRegistrationRequest: type: object properties: - email: - type: string - format: email first_name: type: string last_name: type: string - number_of_coop_shares: + is_company: + type: boolean + is_investing: + type: boolean + num_shares: type: integer + company_name: + type: string + usage_name: + type: string + pronouns: + type: string + birthdate: + type: string + format: date + preferred_language: + $ref: '#/components/schemas/PreferredLanguageEnum' + street: + type: string + city: + type: string + postcode: + type: string + country: + type: string + email: + type: string + format: email + phone: + type: string + other_comments: + type: string required: + - birthdate + - city + - country - email - first_name + - is_company + - is_investing - last_name - - number_of_coop_shares + - num_shares + - postcode + - preferred_language + - street + PreferredLanguageEnum: + enum: + - de + - en + type: string + description: |- + * `de` - 🇩🇪 Deutsch + * `en` - 🇬🇧 English ShareOwnerForWelcomeDesk: type: object properties: diff --git a/src/api-client/.openapi-generator/FILES b/src/api-client/.openapi-generator/FILES index c3d7a3fa3..48fa08e2e 100644 --- a/src/api-client/.openapi-generator/FILES +++ b/src/api-client/.openapi-generator/FILES @@ -7,6 +7,7 @@ models/Column.ts models/DatapointExport.ts models/Dataset.ts models/MemberRegistrationRequest.ts +models/PreferredLanguageEnum.ts models/ShareOwnerForWelcomeDesk.ts models/index.ts runtime.ts diff --git a/src/api-client/models/MemberRegistrationRequest.ts b/src/api-client/models/MemberRegistrationRequest.ts index e4fc538a5..bdb004b11 100644 --- a/src/api-client/models/MemberRegistrationRequest.ts +++ b/src/api-client/models/MemberRegistrationRequest.ts @@ -13,6 +13,14 @@ */ import { mapValues } from '../runtime'; +import type { PreferredLanguageEnum } from './PreferredLanguageEnum'; +import { + PreferredLanguageEnumFromJSON, + PreferredLanguageEnumFromJSONTyped, + PreferredLanguageEnumToJSON, + PreferredLanguageEnumToJSONTyped, +} from './PreferredLanguageEnum'; + /** * * @export @@ -24,35 +32,123 @@ export interface MemberRegistrationRequest { * @type {string} * @memberof MemberRegistrationRequest */ - email: string; + firstName: string; /** * * @type {string} * @memberof MemberRegistrationRequest */ - firstName: string; + lastName: string; /** * - * @type {string} + * @type {boolean} * @memberof MemberRegistrationRequest */ - lastName: string; + isCompany: boolean; + /** + * + * @type {boolean} + * @memberof MemberRegistrationRequest + */ + isInvesting: boolean; /** * * @type {number} * @memberof MemberRegistrationRequest */ - numberOfCoopShares: number; + numShares: number; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + companyName?: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + usageName?: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + pronouns?: string; + /** + * + * @type {Date} + * @memberof MemberRegistrationRequest + */ + birthdate: Date; + /** + * + * @type {PreferredLanguageEnum} + * @memberof MemberRegistrationRequest + */ + preferredLanguage: PreferredLanguageEnum; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + street: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + city: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + postcode: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + country: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + email: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + phone?: string; + /** + * + * @type {string} + * @memberof MemberRegistrationRequest + */ + otherComments?: string; } + + /** * Check if a given object implements the MemberRegistrationRequest interface. */ export function instanceOfMemberRegistrationRequest(value: object): value is MemberRegistrationRequest { - if (!('email' in value) || value['email'] === undefined) return false; if (!('firstName' in value) || value['firstName'] === undefined) return false; if (!('lastName' in value) || value['lastName'] === undefined) return false; - if (!('numberOfCoopShares' in value) || value['numberOfCoopShares'] === undefined) return false; + if (!('isCompany' in value) || value['isCompany'] === undefined) return false; + if (!('isInvesting' in value) || value['isInvesting'] === undefined) return false; + if (!('numShares' in value) || value['numShares'] === undefined) return false; + if (!('birthdate' in value) || value['birthdate'] === undefined) return false; + if (!('preferredLanguage' in value) || value['preferredLanguage'] === undefined) return false; + if (!('street' in value) || value['street'] === undefined) return false; + if (!('city' in value) || value['city'] === undefined) return false; + if (!('postcode' in value) || value['postcode'] === undefined) return false; + if (!('country' in value) || value['country'] === undefined) return false; + if (!('email' in value) || value['email'] === undefined) return false; return true; } @@ -66,10 +162,23 @@ export function MemberRegistrationRequestFromJSONTyped(json: any, ignoreDiscrimi } return { - 'email': json['email'], 'firstName': json['first_name'], 'lastName': json['last_name'], - 'numberOfCoopShares': json['number_of_coop_shares'], + 'isCompany': json['is_company'], + 'isInvesting': json['is_investing'], + 'numShares': json['num_shares'], + 'companyName': json['company_name'] == null ? undefined : json['company_name'], + 'usageName': json['usage_name'] == null ? undefined : json['usage_name'], + 'pronouns': json['pronouns'] == null ? undefined : json['pronouns'], + 'birthdate': (new Date(json['birthdate'])), + 'preferredLanguage': PreferredLanguageEnumFromJSON(json['preferred_language']), + 'street': json['street'], + 'city': json['city'], + 'postcode': json['postcode'], + 'country': json['country'], + 'email': json['email'], + 'phone': json['phone'] == null ? undefined : json['phone'], + 'otherComments': json['other_comments'] == null ? undefined : json['other_comments'], }; } @@ -84,10 +193,23 @@ export function MemberRegistrationRequestFromJSONTyped(json: any, ignoreDiscrimi return { - 'email': value['email'], 'first_name': value['firstName'], 'last_name': value['lastName'], - 'number_of_coop_shares': value['numberOfCoopShares'], + 'is_company': value['isCompany'], + 'is_investing': value['isInvesting'], + 'num_shares': value['numShares'], + 'company_name': value['companyName'], + 'usage_name': value['usageName'], + 'pronouns': value['pronouns'], + 'birthdate': ((value['birthdate']).toISOString().substring(0,10)), + 'preferred_language': PreferredLanguageEnumToJSON(value['preferredLanguage']), + 'street': value['street'], + 'city': value['city'], + 'postcode': value['postcode'], + 'country': value['country'], + 'email': value['email'], + 'phone': value['phone'], + 'other_comments': value['otherComments'], }; } diff --git a/src/api-client/models/PreferredLanguageEnum.ts b/src/api-client/models/PreferredLanguageEnum.ts new file mode 100644 index 000000000..d4099b074 --- /dev/null +++ b/src/api-client/models/PreferredLanguageEnum.ts @@ -0,0 +1,54 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * * `de` - 🇩🇪 Deutsch + * * `en` - 🇬🇧 English + * @export + */ +export const PreferredLanguageEnum = { + De: 'de', + En: 'en' +} as const; +export type PreferredLanguageEnum = typeof PreferredLanguageEnum[keyof typeof PreferredLanguageEnum]; + + +export function instanceOfPreferredLanguageEnum(value: any): boolean { + for (const key in PreferredLanguageEnum) { + if (Object.prototype.hasOwnProperty.call(PreferredLanguageEnum, key)) { + if (PreferredLanguageEnum[key as keyof typeof PreferredLanguageEnum] === value) { + return true; + } + } + } + return false; +} + +export function PreferredLanguageEnumFromJSON(json: any): PreferredLanguageEnum { + return PreferredLanguageEnumFromJSONTyped(json, false); +} + +export function PreferredLanguageEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): PreferredLanguageEnum { + return json as PreferredLanguageEnum; +} + +export function PreferredLanguageEnumToJSON(value?: PreferredLanguageEnum | null): any { + return value as any; +} + +export function PreferredLanguageEnumToJSONTyped(value: any, ignoreDiscriminator: boolean): PreferredLanguageEnum { + return value as PreferredLanguageEnum; +} + diff --git a/src/api-client/models/index.ts b/src/api-client/models/index.ts index 00eb2b381..97d91d326 100644 --- a/src/api-client/models/index.ts +++ b/src/api-client/models/index.ts @@ -4,4 +4,5 @@ export * from './Column'; export * from './DatapointExport'; export * from './Dataset'; export * from './MemberRegistrationRequest'; +export * from './PreferredLanguageEnum'; export * from './ShareOwnerForWelcomeDesk'; diff --git a/src/member_registration/DataProcessingAgreement.tsx b/src/member_registration/DataProcessingAgreement.tsx new file mode 100644 index 000000000..3dc19aabf --- /dev/null +++ b/src/member_registration/DataProcessingAgreement.tsx @@ -0,0 +1,45 @@ +import { COOP_NAME, COOP_STREET, COOP_PLACE } from "./constants"; + +export default function DataProcessingAgreement() { + return ( +
+ Datenschutzerklärung +

+ Verantwortlich für die Datenverarbeitung ist die {COOP_NAME},{" "} + {COOP_STREET}, {COOP_PLACE}. Erreichbar unter contact@supercoop.de. Der + Name, die Anschrift und das Geburtsdatum werden für die Mitgliederliste + der Genossenschaft benötigt (Art. 6 Absatz 1 c DS-GVO i.V.m. §30 Absatz + 2 Satz 1 Nr.1 GenG). Über die Adresse oder die E-Mail-Adresse werden Sie + von der Genossenschaft zu Versammlungen eingeladen (Art. 6 Absatz 1c + DS-GVO i.V.m §46 Absatz 1 Satz 1 GenG i.V.m. §6 Nr.4 GenG) und im Rahmen + der Mitgliedschaft über Angebote der Genossenschaft informiert (Art.6 + Absatz 1 b DS-GVO i.V.m. §1 Absatz 1 GenG i.V.m. der Satzung). Die + Genossenschaft hat ein berechtigtes Interesse an einer unkomplizierten + und rechtssicheren Erfüllung ihrer Verbindlichkeiten. Die Bereitstellung + der personenbezogenen Daten ist gesetzlich bzw. durch die Satzung + vorgeschrieben, die Nichtbereitstellung hätte zur Folge, dass die + Mitgliedschaft nicht zustande kommen kann. +

+

+ Die personenbezogenen Daten werden nicht an Dritte weitergeleitet, + soweit nicht im Einzelfall dafür eine Einwilligung erteilt wird. Wir + sind allerdings gesetzlich verpflichtet, in einigen Fällen Dritten die + Einsicht in die personenbezogenen Daten zu gewähren. Das betrifft zum + Beispiel andere Mitglieder, den gesetzlichen Prüfungsverband oder + Behörden, insbesondere das Finanzamt. Die Daten werden unterschiedlich + aufbewahrt: Alle steuerlich relevanten Informationen werden zehn Jahre + aufbewahrt (§147 AO). Die Daten in der Mitgliederliste (Name und + Anschrift nach §30 Absatz 2 Satz 1 Nr. 1 GenG) werden auch nach dem + Ausscheiden nicht gelöscht (§30 Absatz 2 Satz 1 Nr. 3 GenG). Sie haben + das Recht auf Auskunft seitens des Verantwortlichen über die + betreffenden personenbezogenen Daten sowie auf Berichtigung oder + Löschung oder auf Einschränkung der Verarbeitung (soweit dem nicht eine + gesetzliche Regelung entgegensteht). Auch kann eine Datenübertragung + angefordert werden, sollte der Unterzeichnende eine Übertragung seiner + Daten an eine dritte Stelle wünschen. Darüber hinaus haben Sie das Recht + auf Beschwerde bei einer Aufsichtsbehörde (Landesbeauftragte für + Datenschutz). +

+
+ ); +} diff --git a/src/member_registration/Declarations.tsx b/src/member_registration/Declarations.tsx new file mode 100644 index 000000000..042a4222b --- /dev/null +++ b/src/member_registration/Declarations.tsx @@ -0,0 +1,150 @@ +import { Form } from "react-bootstrap"; +import { MEMBERSHIP_FEE, SHARE_PRICE, COOP_NAME } from "./constants"; +import DataProcessingAgreement from "./DataProcessingAgreement"; +import { useMemo } from "react"; + +declare let gettext: (english_text: string) => string; + +type Props = { + firstName: string; + lastName: string; + shares: number; + acceptsMembership: boolean; + setAcceptsMembership: React.Dispatch>; + acceptsPeriod: boolean; + setAcceptsPeriod: React.Dispatch>; + acceptsConstitution: boolean; + setAcceptsConstitution: React.Dispatch>; + acceptsPayment: boolean; + setAcceptsPayment: React.Dispatch>; + acceptsPrivacy: boolean; + setAcceptsPrivacy: React.Dispatch>; +}; + +export default function Declarations({ + firstName, + lastName, + shares, + acceptsMembership, + setAcceptsMembership, + acceptsPeriod, + setAcceptsPeriod, + acceptsConstitution, + setAcceptsConstitution, + acceptsPayment, + setAcceptsPayment, + acceptsPrivacy, + setAcceptsPrivacy, +}: Props) { + const paymentTotal = useMemo( + () => shares * SHARE_PRICE + MEMBERSHIP_FEE, + [shares], + ); + + return ( + <> +
{gettext("Declarations")}
+ + setAcceptsMembership(event.target.checked)} + required + /> +

+ {gettext( + "In accordance with the bylaws and the law, I agree to purchase shares at a price of", + )}{" "} + {SHARE_PRICE}€ + {gettext(" per share, as well as the membership fee of ")} + {MEMBERSHIP_FEE}€ + {gettext( + ", which will be used to cover administrative costs. I agree to transfer ", + )}{" "} + {paymentTotal}€ + {gettext(" in total")}. +

+
+ + setAcceptsConstitution(event.target.checked)} + required + /> +

+ + SuperCoop - {gettext("Bylaws")} + +

+
+ + setAcceptsPeriod(event.target.checked)} + required + /> + + + setAcceptsPayment(event.target.checked)} + required + /> +

+ {gettext("Account Owner")}: {COOP_NAME} +
+ IBAN: DE98 4306 0967 1121 3790 00 +
+ BIC: GENODEM1GLS +
+ {gettext("Subject")}:{" "} + {[firstName, lastName].join(" ")}: Anteil und Eintrittsgeld +
+ {gettext("Amount")}: {paymentTotal}€ +
+

+
+ + setAcceptsPrivacy(event.target.checked)} + required + /> + + + + ); +} diff --git a/src/member_registration/Error.tsx b/src/member_registration/Error.tsx new file mode 100644 index 000000000..9ce11acbf --- /dev/null +++ b/src/member_registration/Error.tsx @@ -0,0 +1,26 @@ +import { XCircleFill } from "react-bootstrap-icons"; +import { EMAIL_ADDRESS_MEMBER_OFFICE } from "./constants"; + +declare let gettext: (english_text: string) => string; + +export default function Error() { + return ( + <> +
+ +
+
{gettext("Oops! We could not process your application :(")}
+

+ {gettext( + `Please try again later. If you keep having issues, please contact the Members Office at`, + )}{" "} + + {EMAIL_ADDRESS_MEMBER_OFFICE} + +

+ + ); +} diff --git a/src/member_registration/Intro.tsx b/src/member_registration/Intro.tsx new file mode 100644 index 000000000..4541838e7 --- /dev/null +++ b/src/member_registration/Intro.tsx @@ -0,0 +1,21 @@ +declare let gettext: (english_text: string) => string; + +export default function Intro() { + return ( +
+

+ +

+

+ {gettext(` +Welcome to SuperCoop! We're excited to welcome you as a new member of our cooperative. +Please fill out the form below so we can process your application. +`)} +

+
+
+ ); +} diff --git a/src/member_registration/MemberRegistration.tsx b/src/member_registration/MemberRegistration.tsx new file mode 100644 index 000000000..151a0dffa --- /dev/null +++ b/src/member_registration/MemberRegistration.tsx @@ -0,0 +1,323 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Card, Form } from "react-bootstrap"; +import { useApi } from "../hooks/useApi.ts"; +import { CoopApi, MemberRegistrationRequest } from "../api-client/index.ts"; +import TapirButton from "../components/TapirButton.tsx"; +import { ChevronLeft, ChevronRight, Send } from "react-bootstrap-icons"; +import PersonalInfo from "./form_parts/PersonalInfo.tsx"; +import CompanyInfo from "./form_parts/CompanyInfo.tsx"; +import Membership from "./form_parts/Membership.tsx"; +import JoiningAs from "./form_parts/JoiningAs.tsx"; +import ContactInfo from "./form_parts/ContactInfo.tsx"; +import Overview from "./Overview.tsx"; +import Declarations from "./Declarations.tsx"; +import Intro from "./Intro.tsx"; +import Success from "./Success.tsx"; +import Error from "./Error.tsx"; +import { OTHER_COMMENTS_MAX_LENGTH, PreferredLanguage } from "./constants.ts"; +import { getNavigatorLanguage } from "./util.ts"; + +declare let gettext: (english_text: string) => string; + +enum RegistrationStage { + ONE, + TWO, + SUCCESS, + ERROR, +} + +const MemberRegistration: React.FC = () => { + const coopApi = useApi(CoopApi); + const [stage, setStage] = useState(RegistrationStage.ONE); + + const [shares, setShares] = useState(1); + const [isCompany, setIsCompany] = useState(null); + const [isInvesting, setIsInvesting] = useState(false); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + + const [companyName, setCompanyName] = useState(""); + const [preferredName, setPreferredName] = useState(""); + const [pronouns, setPronouns] = useState(""); + const [dob, setDOB] = useState(""); + const [preferredLanguage, setPreferredLanguage] = useState( + getNavigatorLanguage() || PreferredLanguage.GERMAN, + ); + + const [street, setStreet] = useState(""); + const [city, setCity] = useState("Berlin"); + const [postcode, setPostcode] = useState(""); + const [country, setCountry] = useState("DE"); + const [email, setEmail] = useState(""); + const [phone, setPhone] = useState(""); + const [loading, setLoading] = useState(false); + + const [acceptsMembership, setAcceptsMembership] = useState(false); + const [acceptsPeriod, setAcceptsPeriod] = useState(false); + const [acceptsConstitution, setAcceptsConstitution] = useState(false); + const [acceptsPayment, setAcceptsPayment] = useState(false); + const [acceptsPrivacy, setAcceptsPrivacy] = useState(false); + const [otherComments, setOtherComments] = useState(""); + + const [validated, setValidated] = useState(false); + + const topRef = useRef(null); + + useEffect(() => { + if (topRef.current) { + (topRef.current as HTMLHeadingElement).scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }, [stage]); + + const onConfirmRegister = useCallback(() => { + setLoading(true); + + const memberRegistrationRequest: MemberRegistrationRequest = { + firstName, + lastName, + isCompany: !!isCompany, + isInvesting, + numShares: shares, + + birthdate: new Date(dob), + usageName: preferredName, + pronouns, + preferredLanguage, + + street, + city, + postcode, + country, + + email, + phone, + }; + + if (companyName) memberRegistrationRequest.companyName = companyName; + if (otherComments) memberRegistrationRequest.otherComments = otherComments; + + coopApi + .coopMemberSelfRegisterCreate({ + memberRegistrationRequest, + }) + .then((result) => { + if (result) { + setStage(RegistrationStage.SUCCESS); + } else { + setStage(RegistrationStage.ERROR); + } + }) + .catch((error) => { + setStage(RegistrationStage.ERROR); + console.error(error); + }) + .finally(() => setLoading(false)); + }, [ + city, + companyName, + coopApi, + country, + dob, + email, + firstName, + isCompany, + isInvesting, + lastName, + otherComments, + phone, + postcode, + preferredLanguage, + preferredName, + pronouns, + shares, + street, + ]); + + return ( + + +
{gettext("Become a SuperCoop Member!")}
+
+ + {[RegistrationStage.ONE, RegistrationStage.TWO].includes(stage) && ( + + )} + {stage === RegistrationStage.ONE && ( +
+
{gettext("Step 1 - Your Membership")}
+ + {isCompany !== null && ( + <> + + {!isCompany && ( + + )} + {isCompany && ( + + )} + + + { + event.preventDefault(); + if (!event.currentTarget.form?.checkValidity()) { + setValidated(true); + return; + } + + setValidated(false); + setStage(RegistrationStage.TWO); + }} + /> + + + )} + + )} + {stage === RegistrationStage.TWO && ( +
+
+ {gettext("Step 2 - Overview & Declarations")} +
+ + +
+ + {gettext("Other comments")} + setOtherComments(event.target.value)} + /> + +
+ setStage(RegistrationStage.ONE)} + /> + { + event.preventDefault(); + + if (!event.currentTarget.form?.checkValidity()) { + setValidated(true); + return; + } + + onConfirmRegister(); + }} + disabled={ + !acceptsConstitution || + !acceptsMembership || + !acceptsPayment || + !acceptsPeriod || + !acceptsPrivacy + } + loading={loading} + /> +
+ + )} + {stage === RegistrationStage.SUCCESS && ( + + )} + {stage === RegistrationStage.ERROR && } +
+
+ ); +}; + +export default MemberRegistration; diff --git a/src/member_registration/MemberRegistrationCard.tsx b/src/member_registration/MemberRegistrationCard.tsx deleted file mode 100644 index 4d6c566d8..000000000 --- a/src/member_registration/MemberRegistrationCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState } from "react"; -import { Card, Form } from "react-bootstrap"; -import { useApi } from "../hooks/useApi.ts"; -import { CoopApi } from "../api-client"; -import TapirButton from "../components/TapirButton.tsx"; -import { Floppy } from "react-bootstrap-icons"; - -declare let gettext: (english_text: string) => string; - -const MemberRegistrationCard: React.FC = () => { - const coopApi = useApi(CoopApi); - const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(false); - - function onConfirmRegister() { - setLoading(true); - - coopApi - .coopMemberSelfRegisterCreate({ - memberRegistrationRequest: { - email: email, - firstName: "placeholder", - lastName: "placeholder", - numberOfCoopShares: -1, - }, - }) - .then((result) => { - if (result) { - alert("Success!"); - } else { - alert("Failed!"); - } - }) - .catch((error) => { - alert("Request failed! Check the console log"); - console.error(error); - }); - } - - return ( - - -
{gettext("Member self register")}
-
- -
Hello Hackathon :)
-
- - E-Mail - setEmail(event.target.value)} - /> - -
-
- -
-
-
- ); -}; - -export default MemberRegistrationCard; diff --git a/src/member_registration/Overview.tsx b/src/member_registration/Overview.tsx new file mode 100644 index 000000000..4e6584ed7 --- /dev/null +++ b/src/member_registration/Overview.tsx @@ -0,0 +1,78 @@ +declare let gettext: (english_text: string) => string; + +type Props = { + isCompany: boolean | null; + firstName: string; + lastName: string; + preferredName: string; + pronouns: string; + dob: string; + companyName: string; + street: string; + postcode: string; + city: string; + country: string; + email: string; + phone: string; +}; + +export default function Overview({ + isCompany, + firstName, + lastName, + preferredName, + pronouns, + dob, + companyName, + street, + postcode, + city, + country, + email, + phone, +}: Props) { + return ( +
+
+
{gettext("Name:")}
+
{[firstName, lastName].join(" ")}
+
+ {isCompany && ( + <> +
+
{gettext("Company Name:")}
+
{companyName}
+
+ + )} + {!isCompany && ( + <> +
+
{gettext("Preferred name:")}
+
{preferredName || "-"}
+
+
+
{gettext("Pronouns:")}
+
{pronouns || "-"}
+
+
+
{gettext("Date of birth:")}
+
{new Date(dob).toLocaleDateString()}
+
+ + )} +
+
{gettext("E-mail:")}
+
{email}
+
+
+
{gettext("Phone:")}
+
{phone || "-"}
+
+
+
{gettext("Address:")}
+
{[street, postcode, city, country].join(", ")}
+
+
+ ); +} diff --git a/src/member_registration/Success.tsx b/src/member_registration/Success.tsx new file mode 100644 index 000000000..9748e776c --- /dev/null +++ b/src/member_registration/Success.tsx @@ -0,0 +1,33 @@ +import { CheckCircleFill } from "react-bootstrap-icons"; +import { EMAIL_ADDRESS_MEMBER_OFFICE } from "./constants"; + +declare let gettext: (english_text: string) => string; + +type Props = { + name: string; +}; + +export default function Success({ name }: Props) { + return ( + <> +
+ +
+
+ {gettext("Thank you for joining")}, {name} {"<3"} +
+

+ {gettext( + `We have received your application and will let you know via e-mail once it has been processed. + Should you have any questions about your membership, please write to`, + )}{" "} + + {EMAIL_ADDRESS_MEMBER_OFFICE} + +

+ + ); +} diff --git a/src/member_registration/constants.ts b/src/member_registration/constants.ts new file mode 100644 index 000000000..c2acdb3ea --- /dev/null +++ b/src/member_registration/constants.ts @@ -0,0 +1,255 @@ +export const SHARE_PRICE = 100; +export const MEMBERSHIP_FEE = 10; +export const COOP_NAME = "SuperCoop Berlin eG"; +export const COOP_STREET = "Oudenarder Straße 16"; +export const COOP_PLACE = "13347 Berlin"; +export const MIN_REGISTRATION_AGE = 18; +export const EMAIL_ADDRESS_MEMBER_OFFICE = "mitglied@supercoop.de"; +export const OTHER_COMMENTS_MAX_LENGTH = 10_000; + +export enum PreferredLanguage { + ENGLISH = "en", + GERMAN = "de", +} + +export const countries = [ + "AD", // "Andorra", + "AE", // "United Arab Emirates", + "AF", // "Afghanistan", + "AG", // "Antigua & Barbuda", + "AI", // "Anguilla", + "AL", // "Albania", + "AM", // "Armenia", + "AN", // "Netherlands Antilles", + "AO", // "Angola", + "AQ", // "Antarctica", + "AR", // "Argentina", + "AS", // "American Samoa", + "AT", // "Austria", + "AU", // "Australia", + "AW", // "Aruba", + "AZ", // "Azerbaijan", + "BA", // "Bosnia and Herzegovina", + "BB", // "Barbados", + "BD", // "Bangladesh", + "BE", // "Belgium", + "BF", // "Burkina Faso", + "BG", // "Bulgaria", + "BH", // "Bahrain", + "BI", // "Burundi", + "BJ", // "Benin", + "BM", // "Bermuda", + "BN", // "Brunei Darussalam", + "BO", // "Bolivia", + "BR", // "Brazil", + "BS", // "Bahama", + "BT", // "Bhutan", + "BV", // "Bouvet Island", + "BW", // "Botswana", + "BY", // "Belarus", + "BZ", // "Belize", + "CA", // "Canada", + "CC", // "Cocos (Keeling) Islands", + "CF", // "Central African Republic", + "CG", // "Congo", + "CH", // "Switzerland", + "CI", // "Ivory Coast", + "CK", // "Cook Iislands", + "CL", // "Chile", + "CM", // "Cameroon", + "CN", // "China", + "CO", // "Colombia", + "CR", // "Costa Rica", + "CU", // "Cuba", + "CV", // "Cape Verde", + "CX", // "Christmas Island", + "CY", // "Cyprus", + "CZ", // "Czech Republic", + "DE", // "Germany", + "DJ", // "Djibouti", + "DK", // "Denmark", + "DM", // "Dominica", + "DO", // "Dominican Republic", + "DZ", // "Algeria", + "EC", // "Ecuador", + "EE", // "Estonia", + "EG", // "Egypt", + "EH", // "Western Sahara", + "ER", // "Eritrea", + "ES", // "Spain", + "ET", // "Ethiopia", + "FI", // "Finland", + "FJ", // "Fiji", + "FK", // "Falkland Islands (Malvinas)", + "FM", // "Micronesia", + "FO", // "Faroe Islands", + "FR", // "France", + "FX", // "France, Metropolitan", + "GA", // "Gabon", + "GB", // "United Kingdom (Great Britain)", + "GD", // "Grenada", + "GE", // "Georgia", + "GF", // "French Guiana", + "GH", // "Ghana", + "GI", // "Gibraltar", + "GL", // "Greenland", + "GM", // "Gambia", + "GN", // "Guinea", + "GP", // "Guadeloupe", + "GQ", // "Equatorial Guinea", + "GR", // "Greece", + "GS", // "South Georgia and the South Sandwich Islands", + "GT", // "Guatemala", + "GU", // "Guam", + "GW", // "Guinea-Bissau", + "GY", // "Guyana", + "HK", // "Hong Kong", + "HM", // "Heard & McDonald Islands", + "HN", // "Honduras", + "HR", // "Croatia", + "HT", // "Haiti", + "HU", // "Hungary", + "ID", // "Indonesia", + "IE", // "Ireland", + "IL", // "Israel", + "IN", // "India", + "IO", // "British Indian Ocean Territory", + "IQ", // "Iraq", + "IR", // "Islamic Republic of Iran", + "IS", // "Iceland", + "IT", // "Italy", + "JM", // "Jamaica", + "JO", // "Jordan", + "JP", // "Japan", + "KE", // "Kenya", + "KG", // "Kyrgyzstan", + "KH", // "Cambodia", + "KI", // "Kiribati", + "KM", // "Comoros", + "KN", // "St. Kitts and Nevis", + "KP", // "Korea, Democratic People's Republic of", + "KR", // "Korea, Republic of", + "KW", // "Kuwait", + "KY", // "Cayman Islands", + "KZ", // "Kazakhstan", + "LA", // "Lao People's Democratic Republic", + "LB", // "Lebanon", + "LC", // "Saint Lucia", + "LI", // "Liechtenstein", + "LK", // "Sri Lanka", + "LR", // "Liberia", + "LS", // "Lesotho", + "LT", // "Lithuania", + "LU", // "Luxembourg", + "LV", // "Latvia", + "LY", // "Libyan Arab Jamahiriya", + "MA", // "Morocco", + "MC", // "Monaco", + "MD", // "Moldova, Republic of", + "MG", // "Madagascar", + "MH", // "Marshall Islands", + "ML", // "Mali", + "MN", // "Mongolia", + "MM", // "Myanmar", + "MO", // "Macau", + "MP", // "Northern Mariana Islands", + "MQ", // "Martinique", + "MR", // "Mauritania", + "MS", // "Monserrat", + "MT", // "Malta", + "MU", // "Mauritius", + "MV", // "Maldives", + "MW", // "Malawi", + "MX", // "Mexico", + "MY", // "Malaysia", + "MZ", // "Mozambique", + "NA", // "Namibia", + "NC", // "New Caledonia", + "NE", // "Niger", + "NF", // "Norfolk Island", + "NG", // "Nigeria", + "NI", // "Nicaragua", + "NL", // "Netherlands", + "NO", // "Norway", + "NP", // "Nepal", + "NR", // "Nauru", + "NU", // "Niue", + "NZ", // "New Zealand", + "OM", // "Oman", + "PA", // "Panama", + "PE", // "Peru", + "PF", // "French Polynesia", + "PG", // "Papua New Guinea", + "PH", // "Philippines", + "PK", // "Pakistan", + "PL", // "Poland", + "PM", // "St. Pierre & Miquelon", + "PN", // "Pitcairn", + "PR", // "Puerto Rico", + "PT", // "Portugal", + "PW", // "Palau", + "PY", // "Paraguay", + "QA", // "Qatar", + "RE", // "Reunion", + "RO", // "Romania", + "RU", // "Russian Federation", + "RW", // "Rwanda", + "SA", // "Saudi Arabia", + "SB", // "Solomon Islands", + "SC", // "Seychelles", + "SD", // "Sudan", + "SE", // "Sweden", + "SG", // "Singapore", + "SH", // "St. Helena", + "SI", // "Slovenia", + "SJ", // "Svalbard & Jan Mayen Islands", + "SK", // "Slovakia", + "SL", // "Sierra Leone", + "SM", // "San Marino", + "SN", // "Senegal", + "SO", // "Somalia", + "SR", // "Suriname", + "ST", // "Sao Tome & Principe", + "SV", // "El Salvador", + "SY", // "Syrian Arab Republic", + "SZ", // "Swaziland", + "TC", // "Turks & Caicos Islands", + "TD", // "Chad", + "TF", // "French Southern Territories", + "TG", // "Togo", + "TH", // "Thailand", + "TJ", // "Tajikistan", + "TK", // "Tokelau", + "TM", // "Turkmenistan", + "TN", // "Tunisia", + "TO", // "Tonga", + "TP", // "East Timor", + "TR", // "Turkey", + "TT", // "Trinidad & Tobago", + "TV", // "Tuvalu", + "TW", // "Taiwan, Province of China", + "TZ", // "Tanzania, United Republic of", + "UA", // "Ukraine", + "UG", // "Uganda", + "UM", // "United States Minor Outlying Islands", + "US", // "United States of America", + "UY", // "Uruguay", + "UZ", // "Uzbekistan", + "VA", // "Vatican City State (Holy See)", + "VC", // "St. Vincent & the Grenadines", + "VE", // "Venezuela", + "VG", // "British Virgin Islands", + "VI", // "United States Virgin Islands", + "VN", // "Viet Nam", + "VU", // "Vanuatu", + "WF", // "Wallis & Futuna Islands", + "WS", // "Samoa", + "YE", // "Yemen", + "YT", // "Mayotte", + "YU", // "Yugoslavia", + "ZA", // "South Africa", + "ZM", // "Zambia", + "ZR", // "Zaire", + "ZW", // "Zimbabwe", + "ZZ", // "Unknown or unspecified country", +]; diff --git a/src/member_registration/form_parts/CompanyInfo.tsx b/src/member_registration/form_parts/CompanyInfo.tsx new file mode 100644 index 000000000..a08ea7662 --- /dev/null +++ b/src/member_registration/form_parts/CompanyInfo.tsx @@ -0,0 +1,27 @@ +import { Form } from "react-bootstrap"; + +declare let gettext: (english_text: string) => string; + +type Props = { + companyName: string; + setCompanyName: React.Dispatch>; +}; + +export default function CompanyInfo({ companyName, setCompanyName }: Props) { + return ( + + {gettext("What is your company name?")} + setCompanyName(event.target.value)} + required + /> + + {gettext("Please specify the official company name.")} + + + ); +} diff --git a/src/member_registration/form_parts/ContactInfo.tsx b/src/member_registration/form_parts/ContactInfo.tsx new file mode 100644 index 000000000..21b305dee --- /dev/null +++ b/src/member_registration/form_parts/ContactInfo.tsx @@ -0,0 +1,137 @@ +import { Form } from "react-bootstrap"; +import { countries } from "../constants"; + +declare let gettext: (english_text: string) => string; + +const displayNames = new Intl.DisplayNames( + [document.documentElement.getAttribute("lang") ?? navigator.language], + { + type: "region", + }, +); + +type Props = { + street: string; + setStreet: React.Dispatch>; + postcode: string; + setPostcode: React.Dispatch>; + city: string; + setCity: React.Dispatch>; + country: string; + setCountry: React.Dispatch>; + email: string; + setEmail: React.Dispatch>; + phone: string; + setPhone: React.Dispatch>; +}; + +export default function ContactInfo({ + street, + setStreet, + postcode, + setPostcode, + city, + setCity, + country, + setCountry, + email, + setEmail, + phone, + setPhone, +}: Props) { + return ( + <> +
{gettext("Address & Contact Info")}
+ + {gettext("Street & house number")} + setStreet(event.target.value)} + required + /> + + {gettext("Please specify the street address.")} + + + + {gettext("Postcode")} + setPostcode(event.target.value)} + required + /> + + {gettext("Please specify the postal code.")} + + + + {gettext("City")} + setCity(event.target.value)} + required + /> + + {gettext("Please specify the town or city name.")} + + + + {gettext("Country")} + setCountry(event.target.value)} + required + > + {countries.map((code) => ( + + ))} + + + {gettext("Please specify the country name.")} + + + + {gettext("E-mail")} + setEmail(event.target.value)} + autoComplete="email" + required + /> + + {gettext("Please provide a valid e-mail address.")} + + + + {gettext("Phone number")} + setPhone(event.target.value)} + autoComplete="tel" + /> + + {gettext( + "German phone number don't need a prefix (e.g. (0)1736160646), international always (e.g. +12125552368)", + )} + + + + ); +} diff --git a/src/member_registration/form_parts/JoiningAs.tsx b/src/member_registration/form_parts/JoiningAs.tsx new file mode 100644 index 000000000..fca99fa9e --- /dev/null +++ b/src/member_registration/form_parts/JoiningAs.tsx @@ -0,0 +1,46 @@ +import { Form } from "react-bootstrap"; + +declare let gettext: (english_text: string) => string; + +type Props = { + isCompany: boolean | null; + setIsCompany: React.Dispatch>; + setIsInvesting: React.Dispatch>; +}; + +export default function JoiningAs({ + isCompany, + setIsCompany, + setIsInvesting, +}: Props) { + return ( + +
+ {gettext("Are you joining as an individual or company?")} +
+ { + setIsCompany(!event.target.checked); + }} + required + /> + { + setIsCompany(event.target.checked); + setIsInvesting(event.target.checked); + }} + required + /> +
+ ); +} diff --git a/src/member_registration/form_parts/Membership.tsx b/src/member_registration/form_parts/Membership.tsx new file mode 100644 index 000000000..5bb52ff3b --- /dev/null +++ b/src/member_registration/form_parts/Membership.tsx @@ -0,0 +1,114 @@ +import { Col, Form, Row } from "react-bootstrap"; +import { SHARE_PRICE } from "../constants"; + +declare let gettext: (english_text: string) => string; + +type Props = { + firstName: string; + setFirstName: React.Dispatch>; + lastName: string; + setLastName: React.Dispatch>; + shares: number; + setShares: React.Dispatch>; + isInvesting: boolean; + setIsInvesting: React.Dispatch>; +}; + +export default function Membership({ + firstName, + setFirstName, + lastName, + setLastName, + shares, + setShares, + isInvesting, + setIsInvesting, +}: Props) { + return ( + <> +
{gettext("Choose your entry shares")}
+ + + {gettext("How many shares would you like to join with?")} + + setShares(parseInt(event.target.value))} + required + /> + + {gettext("You have to join with 1 or more shares.")} + + + {gettext("You are joining with")}{" "} + {shares * SHARE_PRICE}€ {gettext("worth of shares.")} + + +
{gettext("Choose your membership type")}
+ + setIsInvesting(!event.target.checked)} + required + /> + setIsInvesting(event.target.checked)} + required + /> + + {gettext( + `Investing members are supporters of the Cooperative. They cannot vote in the General Assembly and cannot use the services of the Cooperative.`, + )} + + +
{gettext("Personal details")}
+ + {gettext("What is your name?")} + + + setFirstName(event.target.value)} + autoComplete="first-name" + required + /> + + {gettext("Please specify your first name.")} + + + + setLastName(event.target.value)} + autoComplete="last-name" + required + /> + + {gettext("Please specify your last name.")} + + + + + + ); +} diff --git a/src/member_registration/form_parts/PersonalInfo.tsx b/src/member_registration/form_parts/PersonalInfo.tsx new file mode 100644 index 000000000..14a9bd61f --- /dev/null +++ b/src/member_registration/form_parts/PersonalInfo.tsx @@ -0,0 +1,102 @@ +import { useMemo } from "react"; +import { Form } from "react-bootstrap"; +import { MIN_REGISTRATION_AGE, PreferredLanguage } from "../constants"; + +declare let gettext: (english_text: string) => string; + +type Props = { + preferredName: string; + setPreferredName: React.Dispatch>; + pronouns: string; + setPronouns: React.Dispatch>; + dob: string; + setDOB: React.Dispatch>; + preferredLanguage: PreferredLanguage; + setPreferredLanguage: React.Dispatch>; +}; + +export default function PersonalInfo({ + preferredName, + setPreferredName, + pronouns, + setPronouns, + dob, + setDOB, + preferredLanguage, + setPreferredLanguage, +}: Props) { + const dobMax = useMemo(() => { + const max = new Date(); + max.setFullYear(max.getFullYear() - MIN_REGISTRATION_AGE); + + const yyyy = max.getFullYear().toString().padStart(4, "0"); + const mm = max.getMonth().toString().padStart(2, "0"); + const dd = max.getDay().toString().padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; + }, []); + + return ( + <> + + + {gettext("How would you like to be addressed? (optional)")} + + setPreferredName(event.target.value)} + /> + + + {gettext("What are your pronouns?")} + setPronouns(event.target.value)} + /> + + + {gettext("What is your date of birth?")} + setDOB(event.target.value)} + autoComplete="bday" + max={dobMax} + style={{ width: "auto" }} + required + /> + + {gettext( + "Please specify your date of birth. You must be 18 years or older to become a member.", + )} + + + + {gettext("What is your preferred language?")} + + setPreferredLanguage(event.target.value as PreferredLanguage) + } + style={{ width: "auto" }} + required + > + + + + + {gettext("Please specify your preferred language.")} + + + + ); +} diff --git a/src/member_registration/member_registration_entry.tsx b/src/member_registration/member_registration_entry.tsx index a32035409..ad3491b46 100644 --- a/src/member_registration/member_registration_entry.tsx +++ b/src/member_registration/member_registration_entry.tsx @@ -1,10 +1,10 @@ import { createRoot } from "react-dom/client"; -import MemberRegistrationCard from "./MemberRegistrationCard.tsx"; +import MemberRegistration from "./MemberRegistration.tsx"; const domNode = document.getElementById("member_registration"); if (domNode) { const root = createRoot(domNode); - root.render(); + root.render(); } else { console.error("Failed to render member registration from React"); } diff --git a/src/member_registration/util.ts b/src/member_registration/util.ts new file mode 100644 index 000000000..217d18907 --- /dev/null +++ b/src/member_registration/util.ts @@ -0,0 +1,11 @@ +import { PreferredLanguage } from "./constants"; + +export const getNavigatorLanguage = () => { + const lang = navigator.language.split("-").at(0)?.toLowerCase(); + switch (lang) { + case "en": + return PreferredLanguage.ENGLISH; + default: + return PreferredLanguage.GERMAN; + } +}; diff --git a/tapir/accounts/templates/registration/login.html b/tapir/accounts/templates/registration/login.html index e8223f1d1..3b4631c76 100644 --- a/tapir/accounts/templates/registration/login.html +++ b/tapir/accounts/templates/registration/login.html @@ -11,8 +11,8 @@ {% endblock head %} {% block content %} -
-