Files
portfolio/web/src/components/Social/SocialForm/SocialForm.tsx
Ahmed Al-Taiar 77db153fe6 Add Matrix social
2024-10-15 14:51:43 -04:00

281 lines
8.4 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import {
mdiMenuUp,
mdiMenuDown,
mdiAccount,
mdiRename,
mdiAt,
mdiLinkVariant,
mdiPound,
mdiAccountPlus,
} from '@mdi/js'
import Icon from '@mdi/react'
import type { EditSocialById, UpdateSocialInput } from 'types/graphql'
import type { RWGqlError } from '@redwoodjs/forms'
import { Form, FieldError, Label, TextField, Submit } from '@redwoodjs/forms'
import { baseUrls, getLogoComponent, sortOrder } from 'src/lib/handle'
type FormSocial = NonNullable<EditSocialById['social']>
interface SocialFormProps {
social?: EditSocialById['social']
onSave: (data: UpdateSocialInput, id?: FormSocial['id']) => void
error: RWGqlError
loading: boolean
}
const types: FormSocial['type'][] = [
'x',
'threads',
'instagram',
'facebook',
'tiktok',
'youtube',
'steam',
'discord',
'twitch',
'linkedin',
'matrix',
'github',
'gitea',
'forgejo',
'gitlab',
'bitbucket',
'leetcode',
'email',
'phone',
'custom',
]
const urlHandles: FormSocial['type'][] = ['custom', 'gitea', 'forgejo']
const SocialForm = (props: SocialFormProps) => {
const emailRegex =
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
const phoneRegex = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
const urlRegex =
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i
const matrixRegex =
/^([#@][a-zA-Z0-9_\-\.]+):([a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,})$/
const [type, setType] = useState<FormSocial['type']>(
props.social?.type ?? 'x'
)
const typesDropdownRef = useRef<HTMLDivElement>(null)
const [typesDropdownOpen, setTypesDropdownOpen] = useState(false)
const [username, setUsername] = useState(props.social?.username ?? '')
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
typesDropdownRef.current &&
!typesDropdownRef.current.contains(event.target as Node)
)
setTypesDropdownOpen(false)
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const onSubmit = (data: FormSocial) => {
data.type = type
data.name.trim()
data.username.trim()
props.onSave(data, props?.social?.id)
}
const nameRef = useRef<HTMLInputElement>(null)
useEffect(() => nameRef.current?.focus(), [])
return (
<Form<FormSocial>
onSubmit={onSubmit}
error={props.error}
className="h-128 max-w-80 space-y-2"
>
<Label name="name" className="form-control w-full">
<Label
name="name"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="name"
className="size-4 opacity-70"
errorClassName="size-4 text-error"
>
<Icon path={mdiRename} />
</Label>
<TextField
name="name"
ref={nameRef}
placeholder="Name"
defaultValue={props.social?.name}
className="w-full"
validation={{
required: {
value: true,
message: 'Required',
},
}}
/>
</Label>
<div className="label">
<FieldError
name="name"
className="text-xs font-semibold text-error"
/>
</div>
</Label>
<Label name="username" className="form-control w-full">
<Label
name="username"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="username"
className="size-5 opacity-70"
errorClassName="size-5 text-error"
>
<Icon
path={
type == 'email'
? mdiAt
: type == 'phone'
? mdiPound
: urlHandles.includes(type)
? mdiLinkVariant
: type == 'discord'
? mdiAccountPlus
: mdiAccount
}
/>
</Label>
<TextField
name="username"
placeholder={
urlHandles.includes(type)
? 'URL'
: type == 'phone'
? 'Phone'
: type == 'email'
? 'Email'
: type == 'discord'
? 'Invite Code'
: 'Username'
}
className="w-full"
defaultValue={username}
autoCorrect="off"
autoComplete="off"
onChange={(e) => setUsername(e.target.value)}
validation={{
required: {
value: true,
message: 'Required',
},
pattern: {
value:
type == 'email'
? emailRegex
: type == 'phone'
? phoneRegex
: urlHandles.includes(type)
? urlRegex
: type == 'matrix' && matrixRegex,
message: `Invalid ${
urlHandles.includes(type)
? 'URL'
: type == 'phone'
? 'phone number'
: type == 'email'
? 'Email'
: type == 'matrix' && 'Matrix identifier'
}`,
},
}}
/>
</Label>
<div className="label">
<FieldError
name="username"
className="text-xs font-semibold text-error"
/>
{type == 'phone' && (
<span className="label-text-alt">Format: +1 555-555-5555</span>
)}
{!urlHandles.includes(type) &&
type !== 'phone' &&
type !== 'email' && (
<span className="label-text-alt">{`${baseUrls[type]}${username}`}</span>
)}
</div>
</Label>
<div className="dropdown mb-4" ref={typesDropdownRef}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<div
tabIndex={0}
role="button"
className="btn"
onClick={() => setTypesDropdownOpen(!typesDropdownOpen)}
>
<div className="flex">
{getLogoComponent(type)}
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className="swap swap-flip size-6">
<input type="checkbox" checked={typesDropdownOpen} disabled />
<Icon path={mdiMenuUp} className="swap-on -mr-2 ml-2 size-6" />
<Icon path={mdiMenuDown} className="swap-off -mr-2 ml-2 size-6" />
</label>
</div>
</div>
{typesDropdownOpen && (
<ul
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className="menu dropdown-content z-10 mt-2 grid w-72 grid-cols-5 grid-rows-2 gap-2 rounded-box bg-base-100 shadow-xl"
>
{types
.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b))
.map((type, i) => (
<li key={i}>
<button
className="btn btn-square btn-ghost"
onClick={() => {
setType(type)
setTypesDropdownOpen(false)
}}
>
{getLogoComponent(type)}
</button>
</li>
))}
</ul>
)}
</div>
<nav className="my-2 flex justify-center space-x-2">
<Submit
disabled={props.loading}
className="btn btn-primary btn-sm uppercase"
>
Save
</Submit>
</nav>
</Form>
)
}
export default SocialForm