feat(WEB-170): edit profile view
This commit is contained in:
@@ -115,36 +115,36 @@
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--scandic-blue, #02838E);
|
||||
border: 0.1rem solid var(--scandic-blue, #02838E);
|
||||
color: var(--some-white-color, #FFF);
|
||||
background-color: var(--scandic-blue, #02838e);
|
||||
border: 0.1rem solid var(--scandic-blue, #02838e);
|
||||
color: var(--some-white-color, #fff);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--some-black-color, #000);
|
||||
border: 0.1rem solid var(--some-black-color, #000);
|
||||
color: var(--some-white-color, #FFF);
|
||||
color: var(--some-white-color, #fff);
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
background-color: var(--some-red-color, #D60728);
|
||||
border: 0.1rem solid var(--some-red-color, #D60728);
|
||||
color: var(--some-white-color, #FFF);
|
||||
background-color: var(--some-red-color, #d60728);
|
||||
border: 0.1rem solid var(--some-red-color, #d60728);
|
||||
color: var(--some-white-color, #fff);
|
||||
}
|
||||
|
||||
.quarternary {
|
||||
background-color: var(--some-grey-color, #727272);
|
||||
border: 0.1rem solid var(--some-black-color, #727272);
|
||||
color: var(--some-white-color, #FFF);
|
||||
color: var(--some-white-color, #fff);
|
||||
}
|
||||
|
||||
.white {
|
||||
background-color: var(--some-white-color, #FFF);
|
||||
background-color: var(--some-white-color, #fff);
|
||||
border: 0.1rem solid var(--some-black-color, #000);
|
||||
color: var(--some-black-color, #000);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background-color: var(--some-grey-color, #D9D9D9);
|
||||
.btn:disabled {
|
||||
background-color: var(--some-grey-color, #d9d9d9);
|
||||
color: var(--some-grey-color, #757575);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function Button({
|
||||
bgcolor,
|
||||
className,
|
||||
disabled,
|
||||
intent,
|
||||
size,
|
||||
variant,
|
||||
weight,
|
||||
@@ -20,7 +21,7 @@ export default function Button({
|
||||
const classNames = buttonVariants({
|
||||
bgcolor,
|
||||
className,
|
||||
disabled,
|
||||
intent,
|
||||
size,
|
||||
variant,
|
||||
weight,
|
||||
|
||||
@@ -11,6 +11,10 @@ export const buttonVariants = cva(styles.btn, {
|
||||
quarternary: styles.quarternary,
|
||||
white: styles.white,
|
||||
},
|
||||
intent: {
|
||||
primary: styles.primary,
|
||||
secondary: styles.secondary,
|
||||
},
|
||||
size: {
|
||||
small: styles.small,
|
||||
regular: styles.average,
|
||||
|
||||
256
components/TempDesignSystem/Form/Country/countries.ts
Normal file
256
components/TempDesignSystem/Form/Country/countries.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
export const countries = [
|
||||
{ name: "Afghanistan", code: "AF" },
|
||||
{ name: "Albania", code: "AL" },
|
||||
{ name: "Algeria", code: "DZ" },
|
||||
{ name: "American Samoa", code: "AS" },
|
||||
{ name: "Andorra", code: "AD" },
|
||||
{ name: "Angola", code: "AO" },
|
||||
{ name: "Anguilla", code: "AI" },
|
||||
{ name: "Antarctica", code: "AQ" },
|
||||
{ name: "Antigua and Barbuda", code: "AG" },
|
||||
{ name: "Argentina", code: "AR" },
|
||||
{ name: "Armenia", code: "AM" },
|
||||
{ name: "Aruba", code: "AW" },
|
||||
{ name: "Australia", code: "AU" },
|
||||
{ name: "Austria", code: "AT" },
|
||||
{ name: "Azerbaijan", code: "AZ" },
|
||||
{ name: "Bahamas", code: "BS" },
|
||||
{ name: "Bahrain", code: "BH" },
|
||||
{ name: "Bangladesh", code: "BD" },
|
||||
{ name: "Barbados", code: "BB" },
|
||||
{ name: "Belarus", code: "BY" },
|
||||
{ name: "Belgium", code: "BE" },
|
||||
{ name: "Belize", code: "BZ" },
|
||||
{ name: "Benin", code: "BJ" },
|
||||
{ name: "Bermuda", code: "BM" },
|
||||
{ name: "Bhutan", code: "BT" },
|
||||
{ name: "Bolivia", code: "BO" },
|
||||
{ name: "Bonaire", code: "BQ" },
|
||||
{ name: "Bosnia and Herzegovina", code: "BA" },
|
||||
{ name: "Botswana", code: "BW" },
|
||||
{ name: "Bouvet Island", code: "BV" },
|
||||
{ name: "Brazil", code: "BR" },
|
||||
{ name: "British Indian Ocean Territory", code: "IO" },
|
||||
{ name: "Brunei Darussalam", code: "BN" },
|
||||
{ name: "Bulgaria", code: "BG" },
|
||||
{ name: "Burkina Faso", code: "BF" },
|
||||
{ name: "Burundi", code: "BI" },
|
||||
{ name: "Cambodia", code: "KH" },
|
||||
{ name: "Cameroon", code: "CM" },
|
||||
{ name: "Canada", code: "CA" },
|
||||
{ name: "Cape Verde", code: "CV" },
|
||||
{ name: "Cayman Islands", code: "KY" },
|
||||
{ name: "Central African Republic", code: "CF" },
|
||||
{ name: "Chad", code: "TD" },
|
||||
{ name: "Chile", code: "CL" },
|
||||
{ name: "China", code: "CN" },
|
||||
{ name: "Christmas Island", code: "CX" },
|
||||
{ name: "Cocos (Keeling) Islands", code: "CC" },
|
||||
{ name: "Colombia", code: "CO" },
|
||||
{ name: "Comoros", code: "KM" },
|
||||
{ name: "Congo", code: "CG" },
|
||||
{ name: "Congo, The Democratic Republic of the", code: "CD" },
|
||||
{ name: "Cook Islands", code: "CK" },
|
||||
{ name: "Costa Rica", code: "CR" },
|
||||
{ name: 'Cote D"Ivoire', code: "CI" },
|
||||
{ name: "Croatia", code: "HR" },
|
||||
{ name: "Cuba", code: "CU" },
|
||||
{ name: "Curacao", code: "CW" },
|
||||
{ name: "Cyprus", code: "CY" },
|
||||
{ name: "Czech Republic", code: "CZ" },
|
||||
{ name: "Denmark", code: "DK" },
|
||||
{ name: "Djibouti", code: "DJ" },
|
||||
{ name: "Dominica", code: "DM" },
|
||||
{ name: "Dominican Republic", code: "DO" },
|
||||
{ name: "Ecuador", code: "EC" },
|
||||
{ name: "Egypt", code: "EG" },
|
||||
{ name: "El Salvador", code: "SV" },
|
||||
{ name: "Equatorial Guinea", code: "GQ" },
|
||||
{ name: "Eritrea", code: "ER" },
|
||||
{ name: "Estonia", code: "EE" },
|
||||
{ name: "Eswatini", code: "SZ" },
|
||||
{ name: "Ethiopia", code: "ET" },
|
||||
{ name: "Falkland Islands (Malvinas)", code: "FK" },
|
||||
{ name: "Faroe Islands", code: "FO" },
|
||||
{ name: "Fiji", code: "FJ" },
|
||||
{ name: "Finland", code: "FI" },
|
||||
{ name: "France", code: "FR" },
|
||||
{ name: "French Guiana", code: "GF" },
|
||||
{ name: "French Polynesia", code: "PF" },
|
||||
{ name: "French Southern Territories", code: "TF" },
|
||||
{ name: "Gabon", code: "GA" },
|
||||
{ name: "Gambia", code: "GM" },
|
||||
{ name: "Georgia", code: "GE" },
|
||||
{ name: "Germany", code: "DE" },
|
||||
{ name: "Ghana", code: "GH" },
|
||||
{ name: "Gibraltar", code: "GI" },
|
||||
{ name: "Greece", code: "GR" },
|
||||
{ name: "Greenland", code: "GL" },
|
||||
{ name: "Grenada", code: "GD" },
|
||||
{ name: "Guadeloupe", code: "GP" },
|
||||
{ name: "Guam", code: "GU" },
|
||||
{ name: "Guatemala", code: "GT" },
|
||||
{ name: "Guernsey", code: "GG" },
|
||||
{ name: "Guinea", code: "GN" },
|
||||
{ name: "Guinea-Bissau", code: "GW" },
|
||||
{ name: "Guyana", code: "GY" },
|
||||
{ name: "Haiti", code: "HT" },
|
||||
{ name: "Heard Island and Mcdonald Islands", code: "HM" },
|
||||
{ name: "Holy See (Vatican City State)", code: "VA" },
|
||||
{ name: "Honduras", code: "HN" },
|
||||
{ name: "Hong Kong", code: "HK" },
|
||||
{ name: "Hungary", code: "HU" },
|
||||
{ name: "Iceland", code: "IS" },
|
||||
{ name: "India", code: "IN" },
|
||||
{ name: "Indonesia", code: "ID" },
|
||||
{ name: "Iran, Islamic Republic Of", code: "IR" },
|
||||
{ name: "Iraq", code: "IQ" },
|
||||
{ name: "Ireland", code: "IE" },
|
||||
{ name: "Isle of Man", code: "IM" },
|
||||
{ name: "Israel", code: "IL" },
|
||||
{ name: "Italy", code: "IT" },
|
||||
{ name: "Ivory Coast", code: "CI" },
|
||||
{ name: "Jamaica", code: "JM" },
|
||||
{ name: "Japan", code: "JP" },
|
||||
{ name: "Jersey", code: "JE" },
|
||||
{ name: "Jordan", code: "JO" },
|
||||
{ name: "Kazakhstan", code: "KZ" },
|
||||
{ name: "Kenya", code: "KE" },
|
||||
{ name: "Kiribati", code: "KI" },
|
||||
{ name: 'Korea, Democratic People"S Republic of', code: "KP" },
|
||||
{ name: "Korea, Republic of", code: "KR" },
|
||||
{ name: "Kuwait", code: "KW" },
|
||||
{ name: "Kyrgyzstan", code: "KG" },
|
||||
{ name: 'Lao People"S Democratic Republic', code: "LA" },
|
||||
{ name: "Laos", code: "LA" },
|
||||
{ name: "Latvia", code: "LV" },
|
||||
{ name: "Lebanon", code: "LB" },
|
||||
{ name: "Lesotho", code: "LS" },
|
||||
{ name: "Liberia", code: "LR" },
|
||||
{ name: "Libyan Arab Jamahiriya", code: "LY" },
|
||||
{ name: "Liechtenstein", code: "LI" },
|
||||
{ name: "Lithuania", code: "LT" },
|
||||
{ name: "Luxembourg", code: "LU" },
|
||||
{ name: "Macao", code: "MO" },
|
||||
{ name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
|
||||
{ name: "Madagascar", code: "MG" },
|
||||
{ name: "Malawi", code: "MW" },
|
||||
{ name: "Malaysia", code: "MY" },
|
||||
{ name: "Maldives", code: "MV" },
|
||||
{ name: "Mali", code: "ML" },
|
||||
{ name: "Malta", code: "MT" },
|
||||
{ name: "Marshall Islands", code: "MH" },
|
||||
{ name: "Martinique", code: "MQ" },
|
||||
{ name: "Mauritania", code: "MR" },
|
||||
{ name: "Mauritius", code: "MU" },
|
||||
{ name: "Mayotte", code: "YT" },
|
||||
{ name: "Mexico", code: "MX" },
|
||||
{ name: "Micronesia, Federated States of", code: "FM" },
|
||||
{ name: "Moldova, Republic of", code: "MD" },
|
||||
{ name: "Monaco", code: "MC" },
|
||||
{ name: "Mongolia", code: "MN" },
|
||||
{ name: "Montenegro", code: "ME" },
|
||||
{ name: "Montserrat", code: "MS" },
|
||||
{ name: "Morocco", code: "MA" },
|
||||
{ name: "Mozambique", code: "MZ" },
|
||||
{ name: "Myanmar", code: "MM" },
|
||||
{ name: "Namibia", code: "NA" },
|
||||
{ name: "Nauru", code: "NR" },
|
||||
{ name: "Nepal", code: "NP" },
|
||||
{ name: "Netherlands", code: "NL" },
|
||||
{ name: "Netherlands Antilles", code: "AN" },
|
||||
{ name: "New Caledonia", code: "NC" },
|
||||
{ name: "New Zealand", code: "NZ" },
|
||||
{ name: "Nicaragua", code: "NI" },
|
||||
{ name: "Niger", code: "NE" },
|
||||
{ name: "Nigeria", code: "NG" },
|
||||
{ name: "Niue", code: "NU" },
|
||||
{ name: "Norfolk Island", code: "NF" },
|
||||
{ name: "Northern Mariana Islands", code: "MP" },
|
||||
{ name: "Norway", code: "NO" },
|
||||
{ name: "Oman", code: "OM" },
|
||||
{ name: "Pakistan", code: "PK" },
|
||||
{ name: "Palau", code: "PW" },
|
||||
{ name: "Palestinian Territory, Occupied", code: "PS" },
|
||||
{ name: "Panama", code: "PA" },
|
||||
{ name: "Papua New Guinea", code: "PG" },
|
||||
{ name: "Paraguay", code: "PY" },
|
||||
{ name: "Peru", code: "PE" },
|
||||
{ name: "Philippines", code: "PH" },
|
||||
{ name: "Pitcairn", code: "PN" },
|
||||
{ name: "Poland", code: "PL" },
|
||||
{ name: "Portugal", code: "PT" },
|
||||
{ name: "Puerto Rico", code: "PR" },
|
||||
{ name: "Qatar", code: "QA" },
|
||||
{ name: "RWANDA", code: "RW" },
|
||||
{ name: "Reunion", code: "RE" },
|
||||
{ name: "Romania", code: "RO" },
|
||||
{ name: "Russian Federation", code: "RU" },
|
||||
{ name: "Saint Barthelemy", code: "BL" },
|
||||
{ name: "Saint Helena", code: "SH" },
|
||||
{ name: "Saint Kitts and Nevis", code: "KN" },
|
||||
{ name: "Saint Lucia", code: "LC" },
|
||||
{ name: "Saint Martin", code: "MF" },
|
||||
{ name: "Saint Pierre and Miquelon", code: "PM" },
|
||||
{ name: "Saint Vincent and the Grenadines", code: "VC" },
|
||||
{ name: "Samoa", code: "WS" },
|
||||
{ name: "San Marino", code: "SM" },
|
||||
{ name: "Sao Tome and Principe", code: "ST" },
|
||||
{ name: "Saudi Arabia", code: "SA" },
|
||||
{ name: "Senegal", code: "SN" },
|
||||
{ name: "Serbia", code: "RS" },
|
||||
{ name: "Seychelles", code: "SC" },
|
||||
{ name: "Sierra Leone", code: "SL" },
|
||||
{ name: "Singapore", code: "SG" },
|
||||
{ name: "Sint Maarten", code: "SX" },
|
||||
{ name: "Slovakia", code: "SK" },
|
||||
{ name: "Slovenia", code: "SI" },
|
||||
{ name: "Solomon Islands", code: "SB" },
|
||||
{ name: "Somalia", code: "SO" },
|
||||
{ name: "South Africa", code: "ZA" },
|
||||
{ name: "South Georgia and the South Sandwich Islands", code: "GS" },
|
||||
{ name: "South Sudan", code: "SS" },
|
||||
{ name: "Spain", code: "ES" },
|
||||
{ name: "Sri Lanka", code: "LK" },
|
||||
{ name: "Sudan", code: "SD" },
|
||||
{ name: "Suriname", code: "SR" },
|
||||
{ name: "Svalbard and Jan Mayen", code: "SJ" },
|
||||
{ name: "Swaziland", code: "SZ" },
|
||||
{ name: "Sweden", code: "SE" },
|
||||
{ name: "Switzerland", code: "CH" },
|
||||
{ name: "Syrian Arab Republic", code: "SY" },
|
||||
{ name: "Taiwan", code: "TW" },
|
||||
{ name: "Tajikistan", code: "TJ" },
|
||||
{ name: "Tanzania, United Republic of", code: "TZ" },
|
||||
{ name: "Thailand", code: "TH" },
|
||||
{ name: "Timor-Leste", code: "TL" },
|
||||
{ name: "Togo", code: "TG" },
|
||||
{ name: "Tokelau", code: "TK" },
|
||||
{ name: "Tonga", code: "TO" },
|
||||
{ name: "Trinidad and Tobago", code: "TT" },
|
||||
{ name: "Tunisia", code: "TN" },
|
||||
{ name: "Turkey", code: "TR" },
|
||||
{ name: "Turkmenistan", code: "TM" },
|
||||
{ name: "Turks and Caicos Islands", code: "TC" },
|
||||
{ name: "Tuvalu", code: "TV" },
|
||||
{ name: "Uganda", code: "UG" },
|
||||
{ name: "Ukraine", code: "UA" },
|
||||
{ name: "United Arab Emirates", code: "AE" },
|
||||
{ name: "United Kingdom", code: "GB" },
|
||||
{ name: "United States", code: "US" },
|
||||
{ name: "United States Minor Outlying Islands", code: "UM" },
|
||||
{ name: "Uruguay", code: "UY" },
|
||||
{ name: "Uzbekistan", code: "UZ" },
|
||||
{ name: "Vanuatu", code: "VU" },
|
||||
{ name: "Venezuela", code: "VE" },
|
||||
{ name: "Viet Nam", code: "VN" },
|
||||
{ name: "Vietnam", code: "VN" },
|
||||
{ name: "Virgin Islands, British", code: "VG" },
|
||||
{ name: "Virgin Islands, U.S.", code: "VI" },
|
||||
{ name: "Wallis and Futuna", code: "WF" },
|
||||
{ name: "Western Sahara", code: "EH" },
|
||||
{ name: "Yemen", code: "YE" },
|
||||
{ name: "Zambia", code: "ZM" },
|
||||
{ name: "Zimbabwe", code: "ZW" },
|
||||
{ name: "Åland Islands", code: "AX" },
|
||||
]
|
||||
61
components/TempDesignSystem/Form/Country/country.module.css
Normal file
61
components/TempDesignSystem/Form/Country/country.module.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.container {
|
||||
--select-border: 2px solid var(--some-black-color, #757575);
|
||||
--select-radius: 0.4rem;
|
||||
--select-width: min(28rem, 100%);
|
||||
}
|
||||
|
||||
.comboBoxContainer {
|
||||
background-color: var(--some-white-color, #fff);
|
||||
display: grid;
|
||||
grid-template-areas: "content";
|
||||
width: var(--select-width);
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: var(--some-white-color, #fff);
|
||||
border: var(--select-border);
|
||||
border-radius: var(--select-radius);
|
||||
grid-area: content;
|
||||
height: 4rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
width: var(--select-width);
|
||||
}
|
||||
|
||||
.input,
|
||||
.listBoxItem {
|
||||
color: var(--some-black-color, #757575);
|
||||
font-family: var(--ff-fira-sans);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -1.5%;
|
||||
line-height: 2.4rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
grid-area: content;
|
||||
height: 100%;
|
||||
justify-self: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 1.6rem;
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: var(--some-white-color, #fff);
|
||||
border: var(--select-border);
|
||||
border-radius: var(--select-radius);
|
||||
overflow: auto;
|
||||
padding: 1.6rem 1.6rem 1.6rem 0.8rem;
|
||||
width: var(--select-width);
|
||||
}
|
||||
|
||||
.listBoxItem {
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
|
||||
.listBoxItem[data-selected="true"],
|
||||
.listBoxItem[data-focused="true"] {
|
||||
background-color: rgba(75, 75, 75, 0.2);
|
||||
}
|
||||
7
components/TempDesignSystem/Form/Country/country.ts
Normal file
7
components/TempDesignSystem/Form/Country/country.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type CountryProps = {
|
||||
name?: string
|
||||
placeholder?: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
109
components/TempDesignSystem/Form/Country/index.tsx
Normal file
109
components/TempDesignSystem/Form/Country/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { _ } from "@/lib/translation"
|
||||
import { countries } from "./countries"
|
||||
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
FieldError,
|
||||
Input,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
type Key,
|
||||
} from "react-aria-components"
|
||||
import { ErrorMessage } from "@hookform/error-message"
|
||||
import SelectChevron from "../SelectChevron"
|
||||
|
||||
import styles from "./country.module.css"
|
||||
|
||||
import type { CountryProps } from "./country"
|
||||
|
||||
export default function CountrySelect({
|
||||
name = "country",
|
||||
placeholder = "Select a country",
|
||||
registerOptions,
|
||||
}: CountryProps) {
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
const [divElement, setDivElement] = useState(divRef.current)
|
||||
const { control, setValue } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
const [selectedKey, setSelectedKey] = useState(() => {
|
||||
if (field.value) {
|
||||
const selected = countries.find(
|
||||
(country) =>
|
||||
country.name === field.value || country.code === field.value
|
||||
)
|
||||
if (selected) {
|
||||
return selected.name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
function handleChange(country: Key) {
|
||||
setSelectedKey(String(country))
|
||||
setValue(name, country)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (divRef.current) {
|
||||
setDivElement(divRef.current)
|
||||
}
|
||||
}, [divRef.current])
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={divRef}>
|
||||
<ComboBox
|
||||
className={styles.select}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onSelectionChange={handleChange}
|
||||
ref={field.ref}
|
||||
selectedKey={selectedKey}
|
||||
>
|
||||
<div className={styles.comboBoxContainer}>
|
||||
<Input className={styles.input} placeholder={_(placeholder)} />
|
||||
<Button className={styles.button}>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError>
|
||||
<ErrorMessage name={name} />
|
||||
</FieldError>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={divElement ?? undefined}
|
||||
>
|
||||
<ListBox>
|
||||
{countries.map((country, idx) => (
|
||||
<ListBoxItem
|
||||
className={styles.listBoxItem}
|
||||
id={country.name}
|
||||
key={`${country.code}-${idx}`}
|
||||
>
|
||||
{country.name}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</ComboBox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
components/TempDesignSystem/Form/Date/Select/index.tsx
Normal file
82
components/TempDesignSystem/Form/Date/Select/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
import { useEffect, useRef, useState, type FocusEvent } from "react"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
Select as ReactAriaSelect,
|
||||
SelectValue,
|
||||
type Key,
|
||||
} from "react-aria-components"
|
||||
import SelectChevron from "../../SelectChevron"
|
||||
|
||||
import styles from "./select.module.css"
|
||||
|
||||
import type { SelectProps } from "./select"
|
||||
|
||||
export default function Select({
|
||||
items,
|
||||
label,
|
||||
name,
|
||||
onSelect,
|
||||
placeholder,
|
||||
value,
|
||||
}: SelectProps) {
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
const [divElement, setDivElement] = useState(divRef.current)
|
||||
|
||||
function handleOnSelect(key: Key) {
|
||||
onSelect(key, name)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (divRef.current) {
|
||||
setDivElement(divRef.current)
|
||||
}
|
||||
}, [divRef.current])
|
||||
|
||||
return (
|
||||
<div className={styles.date} ref={divRef}>
|
||||
<ReactAriaSelect
|
||||
className={styles.select}
|
||||
onSelectionChange={handleOnSelect}
|
||||
placeholder={placeholder}
|
||||
selectedKey={value as Key}
|
||||
>
|
||||
<Label className={styles.label}>{label}</Label>
|
||||
<Button className={styles.input}>
|
||||
<SelectValue />
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={divElement ?? undefined}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
key={item}
|
||||
className={styles.listBoxItem}
|
||||
id={item}
|
||||
textValue={String(item)}
|
||||
>
|
||||
{item}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</ReactAriaSelect>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.date {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: var(--ff-fira-sans);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
align-items: center;
|
||||
background-color: var(--some-white-color, #fff);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--some-black-color, #757575);
|
||||
display: grid;
|
||||
font-family: var(--ff-fira-sans);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr auto;
|
||||
height: 4rem;
|
||||
letter-spacing: -1.5%;
|
||||
line-height: 2.4rem;
|
||||
padding: 0.8rem 1rem 0.8rem 1.6rem;
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: var(--some-white-color, #fff);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listBox {
|
||||
padding: 1.6rem 1.6rem 1.6rem 0.8rem;
|
||||
}
|
||||
|
||||
.listBoxItem {
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
|
||||
.listBoxItem[data-selected="true"],
|
||||
.listBoxItem[data-focused="true"] {
|
||||
background-color: rgba(75, 75, 75, 0.2);
|
||||
}
|
||||
16
components/TempDesignSystem/Form/Date/Select/select.ts
Normal file
16
components/TempDesignSystem/Form/Date/Select/select.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
export const enum DateName {
|
||||
date = "date",
|
||||
month = "month",
|
||||
year = "year",
|
||||
}
|
||||
|
||||
export interface SelectProps
|
||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {
|
||||
items: number[]
|
||||
label: string
|
||||
name: DateName
|
||||
onSelect: (key: Key, name: DateName) => void
|
||||
placeholder?: string
|
||||
}
|
||||
30
components/TempDesignSystem/Form/Date/date.module.css
Normal file
30
components/TempDesignSystem/Form/Date/date.module.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.container {
|
||||
--border: 2px solid var(--some-black-color, #757575);
|
||||
--radius: 0.4rem;
|
||||
--width: min(28rem, 100%);
|
||||
|
||||
--width-day: 6rem;
|
||||
--width-month: 6rem;
|
||||
--width-year: 8rem;
|
||||
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
grid-template-areas: "day month year";
|
||||
grid-template-columns: min(--width-day, 1fr) min(--width-month, 1fr) min(
|
||||
--width-year,
|
||||
2fr
|
||||
);
|
||||
width: var(--width);
|
||||
}
|
||||
|
||||
.day {
|
||||
grid-area: day;
|
||||
}
|
||||
|
||||
.month {
|
||||
grid-area: month;
|
||||
}
|
||||
|
||||
.year {
|
||||
grid-area: year;
|
||||
}
|
||||
9
components/TempDesignSystem/Form/Date/date.ts
Normal file
9
components/TempDesignSystem/Form/Date/date.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Control, RegisterOptions } from "react-hook-form"
|
||||
import type { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
|
||||
export interface DateProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
control: Control<EditProfileSchema>
|
||||
name: keyof EditProfileSchema
|
||||
registerOptions: RegisterOptions<EditProfileSchema>
|
||||
}
|
||||
120
components/TempDesignSystem/Form/Date/index.tsx
Normal file
120
components/TempDesignSystem/Form/Date/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
import { parseDate } from "@internationalized/date"
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||
|
||||
import { _ } from "@/lib/translation"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { rangeArray } from "@/utils/rangeArray"
|
||||
|
||||
import {
|
||||
DateInput,
|
||||
DatePicker,
|
||||
DateSegment,
|
||||
Group,
|
||||
} from "react-aria-components"
|
||||
import Select from "./Select"
|
||||
|
||||
import styles from "./date.module.css"
|
||||
|
||||
import { DateName } from "./Select/select"
|
||||
import type { DateProps } from "./date"
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
/** TODO: Get selecting with Enter-key to work */
|
||||
export default function DateSelect({
|
||||
control,
|
||||
name,
|
||||
registerOptions,
|
||||
}: DateProps) {
|
||||
const d = useWatch({ name })
|
||||
const { setValue } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
const currentYear = new Date().getFullYear()
|
||||
const months = rangeArray(1, 12)
|
||||
const years = rangeArray(1900, currentYear).reverse()
|
||||
|
||||
function handleOnSelect(select: Key, selector: DateName) {
|
||||
/**
|
||||
* Months are 0 index based and therefore we
|
||||
* must subtract by 1 to get the selected month
|
||||
*/
|
||||
if (selector === DateName.month) {
|
||||
select = Number(select) - 1
|
||||
}
|
||||
const newDate = dt(d).set(selector, Number(select))
|
||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
granularity="day"
|
||||
isRequired={!!registerOptions.required}
|
||||
name={name}
|
||||
ref={field.ref}
|
||||
value={parseDate(d)}
|
||||
>
|
||||
<Group>
|
||||
<DateInput className={styles.container}>
|
||||
{(segment) => {
|
||||
switch (segment.type) {
|
||||
case "day":
|
||||
let days = []
|
||||
if (segment.maxValue && segment.minValue) {
|
||||
days = rangeArray(segment.minValue, segment.maxValue)
|
||||
} else {
|
||||
days = Array.from(Array(segment.maxValue).keys()).map(
|
||||
(i) => i + 1
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DateSegment className={styles.day} segment={segment}>
|
||||
<Select
|
||||
items={days}
|
||||
label={_("Day")}
|
||||
name={DateName.date}
|
||||
onSelect={handleOnSelect}
|
||||
placeholder={_("DD")}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
)
|
||||
case "month":
|
||||
return (
|
||||
<DateSegment className={styles.month} segment={segment}>
|
||||
<Select
|
||||
items={months}
|
||||
label={_("Month")}
|
||||
name={DateName.month}
|
||||
onSelect={handleOnSelect}
|
||||
placeholder={_("MM")}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
)
|
||||
case "year":
|
||||
return (
|
||||
<DateSegment className={styles.year} segment={segment}>
|
||||
<Select
|
||||
items={years}
|
||||
label={_("Year")}
|
||||
name={DateName.year}
|
||||
onSelect={handleOnSelect}
|
||||
placeholder={_("YYYY")}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
)
|
||||
default:
|
||||
/** DateInput forces return of ReactElement */
|
||||
return <></>
|
||||
}
|
||||
}}
|
||||
</DateInput>
|
||||
</Group>
|
||||
</DatePicker>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.message {
|
||||
color: var(--some-red-color, #d64242);
|
||||
font-family: var(--ff-fira-sans);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: 2.2rem;
|
||||
margin: 0.4rem 0 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FieldValuesFromFieldErrors } from "@hookform/error-message"
|
||||
import type {
|
||||
FieldErrors,
|
||||
FieldName,
|
||||
FieldValues,
|
||||
Message,
|
||||
MultipleFieldErrors,
|
||||
} from "react-hook-form"
|
||||
|
||||
export type ErrorMessageProps<TFieldErrors> = {
|
||||
errors?: FieldErrors<FieldValues>
|
||||
name: FieldName<FieldValuesFromFieldErrors<TFieldErrors>>
|
||||
message?: Message
|
||||
render?: (data: {
|
||||
message: Message
|
||||
messages?: MultipleFieldErrors
|
||||
}) => React.ReactNode
|
||||
}
|
||||
18
components/TempDesignSystem/Form/ErrorMessage/index.tsx
Normal file
18
components/TempDesignSystem/Form/ErrorMessage/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
|
||||
import type { ErrorMessageProps } from "./errorMessage"
|
||||
|
||||
export default function ErrorMessage<T>({
|
||||
errors,
|
||||
name,
|
||||
}: ErrorMessageProps<T>) {
|
||||
return (
|
||||
<RHFErrorMessage
|
||||
errors={errors}
|
||||
name={name}
|
||||
render={({ message }) => <p className={styles.message}>{message}</p>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
44
components/TempDesignSystem/Form/Input/index.tsx
Normal file
44
components/TempDesignSystem/Form/Input/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
import { useController } from "react-hook-form"
|
||||
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import { Input as AriaInput, TextField } from "react-aria-components"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
|
||||
import type { InputProps } from "./input"
|
||||
|
||||
export default function Input({
|
||||
control,
|
||||
disabled,
|
||||
name,
|
||||
placeholder,
|
||||
registerOptions,
|
||||
type = "text",
|
||||
}: InputProps) {
|
||||
const { field, fieldState, formState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
return (
|
||||
<TextField
|
||||
defaultValue={field.value}
|
||||
isDisabled={disabled ?? field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
type={type}
|
||||
>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</TextField>
|
||||
)
|
||||
}
|
||||
13
components/TempDesignSystem/Form/Input/input.module.css
Normal file
13
components/TempDesignSystem/Form/Input/input.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.input {
|
||||
border: 0.2rem solid var(--some-black-color, #757575);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--some-black-color, #757575);
|
||||
font-family: var(--ff-fira-sans);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
height: 4rem;
|
||||
letter-spacing: -1.5%;
|
||||
line-height: 2.4rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
width: min(28rem, 100%);
|
||||
}
|
||||
9
components/TempDesignSystem/Form/Input/input.ts
Normal file
9
components/TempDesignSystem/Form/Input/input.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
import { Control, RegisterOptions } from "react-hook-form"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
control: Control<EditProfileSchema>
|
||||
name: keyof EditProfileSchema
|
||||
registerOptions?: RegisterOptions<EditProfileSchema>
|
||||
}
|
||||
97
components/TempDesignSystem/Form/Phone/index.tsx
Normal file
97
components/TempDesignSystem/Form/Phone/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
import "react-international-phone/style.css"
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { defaultCountries, getCountry } from "react-international-phone"
|
||||
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import { PhoneInput, type PhoneInputRefType } from "react-international-phone"
|
||||
|
||||
import styles from "./phone.module.css"
|
||||
|
||||
import type { PhoneProps } from "./phone"
|
||||
|
||||
export default function Phone({
|
||||
name = "phone",
|
||||
placeholder = "",
|
||||
registerOptions = {
|
||||
required: true,
|
||||
},
|
||||
}: PhoneProps) {
|
||||
const phoneRef = useRef<PhoneInputRefType>(null)
|
||||
const { control, formState } = useFormContext()
|
||||
const countryValue = useWatch({ name: "country" })
|
||||
const defaultCountry = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "name",
|
||||
value: countryValue,
|
||||
})
|
||||
/**
|
||||
* Holds the previous selected country to be able to update
|
||||
* countrycode based on country select field.
|
||||
* Since PhoneInput inputs the countrys dialcode (country code) upon
|
||||
* selection, we need to check if the current value is just
|
||||
* the previously selected countrys dialcode number.
|
||||
*/
|
||||
const prevSelectedCountry = useRef<string | undefined>(countryValue)
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
const handleCountrySelectForPhone = useCallback(
|
||||
(country: string) => {
|
||||
const selectedCountry = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "name",
|
||||
value: country,
|
||||
})
|
||||
|
||||
if (selectedCountry) {
|
||||
phoneRef.current?.setCountry(selectedCountry.iso2)
|
||||
prevSelectedCountry.current = country
|
||||
}
|
||||
},
|
||||
[phoneRef.current, prevSelectedCountry.current]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (countryValue) {
|
||||
if (field.value) {
|
||||
if (prevSelectedCountry.current) {
|
||||
if (prevSelectedCountry.current !== countryValue) {
|
||||
const selectedCountryPrev = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "name",
|
||||
value: prevSelectedCountry.current,
|
||||
})
|
||||
if (
|
||||
field.value.replace("+", "") === selectedCountryPrev?.dialCode
|
||||
) {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
} else {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
}
|
||||
}, [countryValue, prevSelectedCountry.current])
|
||||
|
||||
return (
|
||||
<div className={styles.phone}>
|
||||
<PhoneInput
|
||||
{...field}
|
||||
className={styles.input}
|
||||
defaultCountry={defaultCountry?.iso2 ?? "se"}
|
||||
placeholder={placeholder}
|
||||
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
||||
ref={phoneRef}
|
||||
/>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
components/TempDesignSystem/Form/Phone/phone.module.css
Normal file
25
components/TempDesignSystem/Form/Phone/phone.module.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.phone {
|
||||
--react-international-phone-border-color: var(--some-black-color, #757575);
|
||||
--react-international-phone-border-radius: 0.4rem;
|
||||
--react-international-phone-font-size: 1.6rem;
|
||||
--react-international-phone-height: 4rem;
|
||||
--react-international-phone-text-color: color:
|
||||
var(--some-black-color, #757575);
|
||||
}
|
||||
|
||||
.phone :global(.react-international-phone-input-container) {
|
||||
display: grid;
|
||||
/* r-i-p sets their width dynamically and doesn't respect the width property of its parent */
|
||||
grid-template-columns: 4.7rem minmax(20.3rem, 1fr);
|
||||
width: min(28rem, 100%);
|
||||
}
|
||||
|
||||
/* react-international-phone only exposes variables to change border-color */
|
||||
.phone :global(.react-international-phone-country-selector-button),
|
||||
.phone :global(.react-international-phone-input) {
|
||||
border-width: 0.2rem;
|
||||
}
|
||||
|
||||
.phone :global(.react-international-phone-input) {
|
||||
padding: 0.8rem 1.6rem;
|
||||
}
|
||||
7
components/TempDesignSystem/Form/Phone/phone.ts
Normal file
7
components/TempDesignSystem/Form/Phone/phone.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type PhoneProps = {
|
||||
name?: string
|
||||
placeholder?: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.chevron {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
div[data-rac][data-open="true"] .chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
11
components/TempDesignSystem/Form/SelectChevron/index.tsx
Normal file
11
components/TempDesignSystem/Form/SelectChevron/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./chevron.module.css"
|
||||
|
||||
export default function SelectChevron() {
|
||||
return (
|
||||
<span aria-hidden="true" className={styles.chevron}>
|
||||
<ChevronDownIcon height={24} width={24} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { linkVariants } from "./variants"
|
||||
|
||||
import NextLink from "next/link"
|
||||
|
||||
import type { LinkProps } from "./link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
export default function Link({
|
||||
className,
|
||||
@@ -14,10 +14,7 @@ export default function Link({
|
||||
variant,
|
||||
...props
|
||||
}: LinkProps) {
|
||||
const currentPageSlug = `/${usePathname()
|
||||
.split("/")
|
||||
.filter((v) => v)
|
||||
.at(-1)}`
|
||||
const currentPageSlug = usePathname()
|
||||
const isActive = currentPageSlug === href
|
||||
const classNames = linkVariants({
|
||||
active: isActive,
|
||||
|
||||
Reference in New Issue
Block a user