5 Commits

Author SHA1 Message Date
f03faabbee UI tweaks on admin side
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 11s
2024-10-23 16:52:10 -04:00
353fb3899e PDF error handling 2024-10-23 12:26:21 -04:00
62ce137bcb Add og metatags to public facing pages
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 11s
2024-10-22 16:48:48 -04:00
f8987b08da Add persistent flag of origin
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 1m12s
2024-10-17 21:36:50 -04:00
cbf75acbeb Fix empty images section if no images + Remove postgres port 2024-10-17 20:46:34 -04:00
44 changed files with 443 additions and 335 deletions

View File

@ -18,6 +18,8 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
FIRST_NAME=firstname FIRST_NAME=firstname
LAST_NAME=lastname LAST_NAME=lastname
COUNTRY=US
SMTP_HOST=smtp.example.com SMTP_HOST=smtp.example.com
SMTP_PORT=465 SMTP_PORT=465
SMTP_SECURE=true SMTP_SECURE=true

View File

@ -6,6 +6,8 @@
FIRST_NAME=firstname FIRST_NAME=firstname
LAST_NAME=lastname LAST_NAME=lastname
COUNTRY=US
SMTP_HOST=smtp.example.com SMTP_HOST=smtp.example.com
SMTP_PORT=465 SMTP_PORT=465
SMTP_SECURE=true SMTP_SECURE=true

View File

@ -21,9 +21,10 @@ services:
- API_PROXY_TARGET=http://localhost:8911 - API_PROXY_TARGET=http://localhost:8911
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60 - MAX_HTTP_CONNECTIONS_PER_MINUTE=60
- SESSION_SECRET=super_secret_session_key_change_me_in_production_please - SESSION_SECRET=super_secret_session_key_change_me_in_production_please
- DATABASE_URL=postgresql://redwood:changeme@db:5432/portfolio - DATABASE_URL=postgresql://redwood:changeme@db/portfolio
- FIRST_NAME=first name # Your first name - FIRST_NAME=first name # Your first name
- LAST_NAME=lastname # Your last name - LAST_NAME=lastname # Your last name
- COUNTRY=US # ISO-3166-1 alpha-2 country code, https://en.wikipedia.org/wiki/ISO_3166-1#Codes
- SMTP_HOST=smtp.example.com - SMTP_HOST=smtp.example.com
- SMTP_PORT=465 - SMTP_PORT=465
- SMTP_SECURE=true - SMTP_SECURE=true
@ -58,8 +59,6 @@ services:
- POSTGRES_USER=redwood - POSTGRES_USER=redwood
- POSTGRES_PASSWORD=changeme - POSTGRES_PASSWORD=changeme
- POSTGRES_DB=portfolio - POSTGRES_DB=portfolio
ports:
- 5432:5432
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
@ -75,9 +74,9 @@ sudo docker exec -u root portfolio chown -R node:node /home/node/app/api/files_p
## Logging In ## Logging In
- Once the container is up and running, head to `/login` (`https://portfolio.example.com/login`), default credentials are below - Once the container is up and running, head to `/login` (`https://portfolio.example.com/login`), default credentials are below
- If you would like to change the password, head to `/forgot-password` (`https://portfolio.example.com/forgot-password`), the username is `admin` - If you would like to change the password, head to `/forgot-password` (`https://portfolio.example.com/forgot-password`), the username is `admin`
- If you correctly set up the Gmail app password, you should receive an email from yourself - If you correctly configured [SMTP](#smtp), you should receive an Email from [`EMAIL_FROM`](#docker-compose) to [`EMAIL_TO`](#docker-compose)
- It contains the link needed to change your password - The Email contains the link needed to change your password
### Default Credentials ### Default Credentials
**Username:** `admin` **Username:** `admin`
**Password:** [`SMTP_PASSWORD`](#smtp) **Password:** [`SMTP_PASSWORD`](#docker-compose)

View File

@ -11,6 +11,7 @@
"@redwoodjs/graphql-server": "8.4.0", "@redwoodjs/graphql-server": "8.4.0",
"@tus/file-store": "^1.4.0", "@tus/file-store": "^1.4.0",
"@tus/server": "^1.7.0", "@tus/server": "^1.7.0",
"country-flag-icons": "^1.5.13",
"graphql-scalars": "^1.23.0", "graphql-scalars": "^1.23.0",
"nodemailer": "^6.9.14" "nodemailer": "^6.9.14"
}, },

View File

@ -10,6 +10,13 @@ import { createServer } from '@redwoodjs/api-server'
import { logger } from 'src/lib/logger' import { logger } from 'src/lib/logger'
import { handleTusUpload } from 'src/lib/tus' import { handleTusUpload } from 'src/lib/tus'
;(async () => { ;(async () => {
const { hasFlag } = await import('country-flag-icons')
if (!hasFlag(process.env.COUNTRY))
throw new Error(
'Invalid COUNTRY environment variable, please select a valid ISO-3166-1 alpha-2 country code\n See https://en.wikipedia.org/wiki/ISO_3166-1#Codes'
)
const server = await createServer({ const server = await createServer({
logger, logger,
configureApiServer: async (server) => { configureApiServer: async (server) => {

View File

@ -9,9 +9,10 @@ services:
- API_PROXY_TARGET=http://localhost:8911 - API_PROXY_TARGET=http://localhost:8911
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60 - MAX_HTTP_CONNECTIONS_PER_MINUTE=60
- SESSION_SECRET=super_secret_session_key_change_me_in_production_please - SESSION_SECRET=super_secret_session_key_change_me_in_production_please
- DATABASE_URL=postgresql://redwood:changeme@db:5432/portfolio - DATABASE_URL=postgresql://redwood:changeme@db/portfolio
- FIRST_NAME=first name # Your first name - FIRST_NAME=first name # Your first name
- LAST_NAME=lastname # Your last name - LAST_NAME=lastname # Your last name
- COUNTRY=US # ISO-3166-1 alpha-2 country code, https://en.wikipedia.org/wiki/ISO_3166-1#Codes
- SMTP_HOST=smtp.example.com - SMTP_HOST=smtp.example.com
- SMTP_PORT=465 - SMTP_PORT=465
- SMTP_SECURE=true - SMTP_SECURE=true
@ -46,8 +47,6 @@ services:
- POSTGRES_USER=redwood - POSTGRES_USER=redwood
- POSTGRES_PASSWORD=changeme - POSTGRES_PASSWORD=changeme
- POSTGRES_DB=portfolio - POSTGRES_DB=portfolio
ports:
- 5432:5432
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data

View File

@ -9,7 +9,7 @@
title = "${FIRST_NAME} ${LAST_NAME}" title = "${FIRST_NAME} ${LAST_NAME}"
port = 8910 port = 8910
apiUrl = "/api" apiUrl = "/api"
includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "API_ADDRESS_PROD", "API_ADDRESS_DEV"] includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "COUNTRY", "API_ADDRESS_PROD", "API_ADDRESS_DEV"]
[generate] [generate]
tests = false tests = false
stories = false stories = false

View File

@ -35,6 +35,7 @@
"@uppy/react": "^4.0.1", "@uppy/react": "^4.0.1",
"@uppy/tus": "^4.0.0", "@uppy/tus": "^4.0.0",
"@uppy/webcam": "^4.0.1", "@uppy/webcam": "^4.0.1",
"country-flag-icons": "^1.5.13",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"humanize-string": "2.1.0", "humanize-string": "2.1.0",
"react": "18.3.1", "react": "18.3.1",

View File

@ -35,7 +35,7 @@ const Routes = () => {
<Route path="/admin/resume" page={ResumeAdminResumePage} name="adminResume" /> <Route path="/admin/resume" page={ResumeAdminResumePage} name="adminResume" />
</Set> </Set>
<Set wrap={ScaffoldLayout} title="Projects" titleTo="projects" buttonLabel="New Project" buttonTo="newProject"> <Set wrap={ScaffoldLayout} title="Projects" titleTo="adminProjects" buttonLabel="New Project" buttonTo="newProject">
<Route path="/admin/projects/new" page={ProjectNewProjectPage} name="newProject" /> <Route path="/admin/projects/new" page={ProjectNewProjectPage} name="newProject" />
<Route path="/admin/projects/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" /> <Route path="/admin/projects/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" />
<Route path="/admin/projects/{id:Int}" page={ProjectAdminProjectPage} name="adminProject" /> <Route path="/admin/projects/{id:Int}" page={ProjectAdminProjectPage} name="adminProject" />

View File

@ -9,54 +9,50 @@ interface ColorPickerProps {
setColor: React.Dispatch<React.SetStateAction<string>> setColor: React.Dispatch<React.SetStateAction<string>>
} }
const ColorPicker = ({ color, setColor }: ColorPickerProps) => { const ColorPicker = ({ color, setColor }: ColorPickerProps) => (
return ( <div className="flex flex-col space-y-2 bg-base-100 p-2 rounded-xl">
<div className="flex flex-col space-y-2 bg-base-100 p-2 rounded-xl w-min"> <HexColorPicker color={color} onChange={setColor} />
<section className="w-52"> <div className="flex space-x-2">
<HexColorPicker color={color} onChange={setColor} /> {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
</section> <label className="input input-bordered flex items-center gap-2 input-sm grow">
<div className="flex space-x-2 w-52"> <Icon path={mdiPound} className="size-4 opacity-70" />
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} <HexColorInput color={color} className="w-16" />
<label className="input input-bordered flex items-center gap-2 input-sm w-full grow"> </label>
<Icon path={mdiPound} className="size-4 opacity-70" /> <button
<HexColorInput color={color} className="w-16" /> type="button"
</label> className="btn btn-square btn-sm"
<button onClick={async () => {
type="button" try {
className="btn btn-square btn-sm" await navigator.clipboard.writeText(color)
onClick={async () => { toast.success('Copied color to clipboard')
try { } catch {
await navigator.clipboard.writeText(color) toast.error(`Failed to copy, please try again`)
toast.success('Copied color to clipboard') }
} catch { }}
toast.error(`Failed to copy, please try again`) >
} <Icon path={mdiContentCopy} className="size-4" />
}} </button>
> <button
<Icon path={mdiContentCopy} className="size-4" /> type="button"
</button> className="btn btn-square btn-sm"
<button onClick={async () => {
type="button" try {
className="btn btn-square btn-sm " const clipboardText = await navigator.clipboard.readText()
onClick={async () => { const hexColorRegex =
try { /^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i
const clipboardText = await navigator.clipboard.readText()
const hexColorRegex =
/^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i
if (!hexColorRegex.test(clipboardText)) if (!hexColorRegex.test(clipboardText))
toast.error(`Text is not a valid hex color`) toast.error(`Text is not a valid hex color`)
else setColor(clipboardText) else setColor(clipboardText)
} catch { } catch {
toast.error(`Failed to paste, please try again`) toast.error(`Failed to paste, please try again`)
} }
}} }}
> >
<Icon path={mdiContentPaste} className="size-4" /> <Icon path={mdiContentPaste} className="size-4" />
</button> </button>
</div>
</div> </div>
) </div>
} )
export default ColorPicker export default ColorPicker

View File

@ -3,10 +3,12 @@ import type {
ContactCardPortraitVariables, ContactCardPortraitVariables,
} from 'types/graphql' } from 'types/graphql'
import type { import { routes } from '@redwoodjs/router'
TypedDocumentNode, import {
CellFailureProps, type TypedDocumentNode,
CellSuccessProps, type CellFailureProps,
type CellSuccessProps,
Metadata,
} from '@redwoodjs/web' } from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
@ -43,5 +45,20 @@ export const Success = ({
portrait, portrait,
socials, socials,
}: CellSuccessProps<ContactCardPortrait, ContactCardPortraitVariables>) => ( }: CellSuccessProps<ContactCardPortrait, ContactCardPortraitVariables>) => (
<ContactCard portraitUrl={portrait.fileId} socials={socials} /> <>
<Metadata
title="Contact"
og={{
title: 'Contact',
type: 'website',
image: {
url: portrait.fileId,
type: 'image/webp',
alt: `${process.env.FIRST_NAME} ${process.env.LAST_NAME}`,
},
url: routes.contact(),
}}
/>
<ContactCard portraitUrl={portrait.fileId} socials={socials} />
</>
) )

View File

@ -1,19 +1,37 @@
import { useState } from 'react'
import { mdiAlertOutline } from '@mdi/js'
import Icon from '@mdi/react'
interface PDFProps { interface PDFProps {
url: string url: string
form?: boolean form?: boolean
} }
const PDF = ({ url, form = false }: PDFProps) => ( const PDF = ({ url, form = false }: PDFProps) => {
<embed const [error, setError] = useState<boolean>(false)
src={url}
title="PDF" return error ? (
type="application/pdf" <div role="alert" className="alert alert-warning">
style={{ <Icon path={mdiAlertOutline} className="size-7" />
width: 'calc(100vw - 1rem)', <span>
height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`, Could not load PDF, this is common in in-app browsers, try opening this
}} page in a regular browser
className="rounded-xl" </span>
/> </div>
) ) : (
<iframe
src={url}
title="PDF"
style={{
width: 'calc(100vw - 1rem)',
height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`,
}}
className="rounded-xl"
onError={() => setError(true)}
onLoad={() => setError(false)}
/>
)
}
export default PDF export default PDF

View File

@ -127,14 +127,13 @@ const PortraitForm = ({ portrait }: PortraitFormProps) => {
) )
else else
return ( return (
<div className="mx-auto w-fit space-y-2"> <div className="mx-auto max-w-prose space-y-2">
{!fileId ? ( {!fileId ? (
<> <>
<Uploader <Uploader
onComplete={onUploadComplete} onComplete={onUploadComplete}
width="22rem" width="auto"
height="34.5rem" height="30rem"
className="flex justify-center"
/> />
<p className="text-center"> <p className="text-center">
High quality, 4:5 aspect ratio image recommended High quality, 4:5 aspect ratio image recommended

View File

@ -46,29 +46,28 @@ const AdminProject = ({ project }: Props) => {
} }
return ( return (
<div className="flex w-full justify-center"> <div className="flex justify-center">
<div> <div>
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>
<th className="w-0"> <th colSpan={2}>
Project {project.id}: {project.title} Project {project.id}: {project.title}
</th> </th>
<th>&nbsp;</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th>ID</th> <th className="text-right">ID</th>
<td>{project.id}</td> <td>{project.id}</td>
</tr> </tr>
<tr> <tr>
<th>Title</th> <th className="text-right">Title</th>
<td>{project.title}</td> <td>{project.title}</td>
</tr> </tr>
<tr> <tr>
<th>Description</th> <th className="text-right">Description</th>
<td> <td>
<article className="prose"> <article className="prose">
{parseHtml(project.description)} {parseHtml(project.description)}
@ -76,11 +75,11 @@ const AdminProject = ({ project }: Props) => {
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Date</th> <th className="text-right">Date</th>
<td>{timeTag(project.date)}</td> <td>{timeTag(project.date)}</td>
</tr> </tr>
<tr> <tr>
<th>Images</th> <th className="text-right">Images</th>
<td> <td>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{project.images.map((image, i) => ( {project.images.map((image, i) => (
@ -98,7 +97,7 @@ const AdminProject = ({ project }: Props) => {
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Tags</th> <th className="text-right">Tags</th>
<td> <td>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{project.tags.map((tag, i) => ( {project.tags.map((tag, i) => (
@ -120,19 +119,30 @@ const AdminProject = ({ project }: Props) => {
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Links</th> <th className="text-right">Links</th>
<td> <td>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{project.links.map((link, i) => ( {project.links.map((link, i) => (
<a <>
href={link} <a
target="_blank" href={link}
className="badge badge-ghost text-nowrap" target="_blank"
key={i} className="hidden sm:flex badge badge-ghost text-nowrap"
rel="noreferrer" key={i}
> rel="noreferrer"
{link} >
</a> {link}
</a>
<a
href={link}
target="_blank"
className="btn btn-sm btn-square sm:hidden"
key={i}
rel="noreferrer"
>
{i + 1}
</a>
</>
))} ))}
</div> </div>
</td> </td>

View File

@ -73,12 +73,12 @@ export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
) => updateProject({ variables: { id, input } }) ) => updateProject({ variables: { id, input } })
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-prose justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table w-80"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>
<th className="w-0">Edit Project {project.id}</th> <th>Edit Project {project.id}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -38,9 +38,9 @@ const NewProject = () => {
createProject({ variables: { input } }) createProject({ variables: { input } })
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-prose justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table w-80"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>
<th>New Project</th> <th>New Project</th>

View File

@ -69,20 +69,23 @@ const Project = ({ project }: Props) => {
</div> </div>
</> </>
)} )}
<h2 className="sm:hidden font-bold text-3xl text-center">Images</h2> {project.images.length > 0 && (
<h2 className="sm:hidden font-bold text-3xl text-center">Images</h2>
)}
</div> </div>
<div className="flex flex-wrap gap-4 items-center pt-8 justify-center"> <div className="flex flex-wrap gap-4 pt-8 justify-center h-fit">
{project.images.map((image, i) => ( {project.images.length > 0 &&
<a project.images.map((image, i) => (
href={image} <a
target="_blank" href={image}
rel="noreferrer" target="_blank"
key={i} rel="noreferrer"
className="rounded-xl" key={i}
> className="rounded-xl"
<img src={image} alt="" className="rounded-xl" /> >
</a> <img src={image} alt="" className="rounded-xl" />
))} </a>
))}
</div> </div>
</div> </div>
) )

View File

@ -1,9 +1,11 @@
import type { FindProjectById, FindProjectByIdVariables } from 'types/graphql' import type { FindProjectById, FindProjectByIdVariables } from 'types/graphql'
import type { import { routes } from '@redwoodjs/router'
CellSuccessProps, import {
CellFailureProps, type CellSuccessProps,
TypedDocumentNode, type CellFailureProps,
type TypedDocumentNode,
Metadata,
} from '@redwoodjs/web' } from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
@ -40,5 +42,23 @@ export const Failure = ({
export const Success = ({ export const Success = ({
project, project,
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => ( }: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => (
<Project project={project} /> <>
<Metadata
title={project.title}
og={{
title: project.title,
type: 'website',
image:
project.images.length > 0
? {
url: project.images[0],
type: 'image/webp',
alt: 'Image 1',
}
: undefined,
url: routes.project({ id: project.id }),
}}
/>
<Project project={project} />
</>
) )

View File

@ -119,7 +119,7 @@ const ProjectForm = (props: ProjectFormProps) => {
<Form<FormProject> <Form<FormProject>
onSubmit={onSubmit} onSubmit={onSubmit}
error={props.error} error={props.error}
className="space-y-2 w-80" className="space-y-2"
> >
<Label name="title" className="form-control w-full"> <Label name="title" className="form-control w-full">
<Label <Label
@ -261,9 +261,8 @@ const ProjectForm = (props: ProjectFormProps) => {
{appendUploader && ( {appendUploader && (
<Uploader <Uploader
onComplete={onUploadComplete} onComplete={onUploadComplete}
width="20rem" width="auto"
height="30rem" height="30rem"
className="flex justify-center"
maxFiles={10} maxFiles={10}
disabled={props.loading} disabled={props.loading}
/> />
@ -272,9 +271,8 @@ const ProjectForm = (props: ProjectFormProps) => {
) : ( ) : (
<Uploader <Uploader
onComplete={onUploadComplete} onComplete={onUploadComplete}
width="20rem" width="auto"
height="30rem" height="30rem"
className="flex justify-center pt-3"
maxFiles={10} maxFiles={10}
disabled={props.loading} disabled={props.loading}
/> />

View File

@ -1,9 +1,11 @@
import type { FindProjects, FindProjectsVariables } from 'types/graphql' import type { FindProjects, FindProjectsVariables } from 'types/graphql'
import type { import { routes } from '@redwoodjs/router'
CellSuccessProps, import {
CellFailureProps, type CellSuccessProps,
TypedDocumentNode, type CellFailureProps,
type TypedDocumentNode,
Metadata,
} from '@redwoodjs/web' } from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
@ -40,5 +42,16 @@ export const Failure = ({ error }: CellFailureProps<FindProjectsVariables>) => (
export const Success = ({ export const Success = ({
projects, projects,
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => ( }: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
<ProjectsShowcase projects={projects} /> <>
<Metadata
title="Projects"
og={{
title: 'Projects',
type: 'website',
description: `${projects.length} projects`,
url: routes.projects(),
}}
/>
<ProjectsShowcase projects={projects} />
</>
) )

View File

@ -1,9 +1,11 @@
import type { FindResume, FindResumeVariables } from 'types/graphql' import type { FindResume, FindResumeVariables } from 'types/graphql'
import type { import { routes } from '@redwoodjs/router'
CellSuccessProps, import {
CellFailureProps, type CellSuccessProps,
TypedDocumentNode, type CellFailureProps,
type TypedDocumentNode,
Metadata,
} from '@redwoodjs/web' } from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
@ -29,5 +31,15 @@ export const Failure = ({ error }: CellFailureProps<FindResumeVariables>) => (
export const Success = ({ export const Success = ({
resume, resume,
}: CellSuccessProps<FindResume, FindResumeVariables>) => ( }: CellSuccessProps<FindResume, FindResumeVariables>) => (
<Resume resume={resume} /> <>
<Metadata
title="Contact"
og={{
title: 'Resume',
type: 'website',
url: routes.resume(),
}}
/>
<Resume resume={resume} />
</>
) )

View File

@ -123,14 +123,13 @@ const ResumeForm = ({ resume }: ResumeFormProps) => {
) )
else else
return ( return (
<div className="mx-auto w-fit space-y-2"> <div className="mx-auto max-w-prose space-y-2">
{!fileId ? ( {!fileId ? (
<> <>
<Uploader <Uploader
onComplete={onUploadComplete} onComplete={onUploadComplete}
width="22rem" width="auto"
height="11.5rem" height="30rem"
className="flex justify-center"
type="pdf" type="pdf"
/> />
</> </>

View File

@ -43,120 +43,122 @@ const RichTextEditor = ({ editor }: RichTextEditorProps) => {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-wrap gap-2 justify-center"> <div className="flex justify-center flex-wrap gap-2">
<button <div className="flex gap-2 h-min justify-center">
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('link') ? 'btn-primary' : ''}`} type="button"
onClick={setLink} className={`btn btn-sm btn-square ${editor.isActive('link') ? 'btn-primary' : ''}`}
> onClick={setLink}
<Icon path={mdiLinkVariant} className="size-5" /> >
</button> <Icon path={mdiLinkVariant} className="size-5" />
<button </button>
type="button" <button
className="btn btn-sm btn-square" type="button"
onClick={() => editor.chain().focus().unsetLink().run()} className="btn btn-sm btn-square"
disabled={!editor.isActive('link')} onClick={() => editor.chain().focus().unsetLink().run()}
> disabled={!editor.isActive('link')}
<Icon path={mdiLinkVariantOff} className="size-5" /> >
</button> <Icon path={mdiLinkVariantOff} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('bulletList') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleBulletList().run()} className={`btn btn-sm btn-square ${editor.isActive('bulletList') ? 'btn-primary' : ''}`}
> onClick={() => editor.chain().focus().toggleBulletList().run()}
<Icon path={mdiFormatListBulleted} className="size-5" /> >
</button> <Icon path={mdiFormatListBulleted} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('orderedList') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleOrderedList().run()} className={`btn btn-sm btn-square ${editor.isActive('orderedList') ? 'btn-primary' : ''}`}
> onClick={() => editor.chain().focus().toggleOrderedList().run()}
<Icon path={mdiFormatListNumbered} className="size-5" /> >
</button> <Icon path={mdiFormatListNumbered} className="size-5" />
<button </button>
type="button" <button
className="btn btn-sm btn-square" type="button"
onClick={() => editor.chain().focus().unsetAllMarks().run()} className="btn btn-sm btn-square"
> onClick={() => editor.chain().focus().unsetAllMarks().run()}
<Icon path={mdiFormatClear} className="size-5" /> >
</button> <Icon path={mdiFormatClear} className="size-5" />
<button </button>
type="button" <button
className="btn btn-sm btn-square" type="button"
onClick={() => editor.chain().focus().undo().run()} className="btn btn-sm btn-square"
disabled={!editor.can().chain().focus().undo().run()} onClick={() => editor.chain().focus().undo().run()}
> disabled={!editor.can().chain().focus().undo().run()}
<Icon path={mdiUndoVariant} className="size-5" /> >
</button> <Icon path={mdiUndoVariant} className="size-5" />
<button </button>
type="button" <button
className="btn btn-sm btn-square" type="button"
onClick={() => editor.chain().focus().redo().run()} className="btn btn-sm btn-square"
disabled={!editor.can().chain().focus().redo().run()} onClick={() => editor.chain().focus().redo().run()}
> disabled={!editor.can().chain().focus().redo().run()}
<Icon path={mdiRedoVariant} className="size-5" /> >
</button> <Icon path={mdiRedoVariant} className="size-5" />
</div> </button>
<div className="flex flex-wrap gap-2 justify-center"> </div>
<button <div className="flex gap-2 h-min justify-center">
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('bold') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleBold().run()} className={`btn btn-sm btn-square ${editor.isActive('bold') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
> disabled={!editor.can().chain().focus().toggleBold().run()}
<Icon path={mdiFormatBold} className="size-5" /> >
</button> <Icon path={mdiFormatBold} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('italic') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleItalic().run()} className={`btn btn-sm btn-square ${editor.isActive('italic') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
> disabled={!editor.can().chain().focus().toggleItalic().run()}
<Icon path={mdiFormatItalic} className="size-5" /> >
</button> <Icon path={mdiFormatItalic} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('underline') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleUnderline().run()} className={`btn btn-sm btn-square ${editor.isActive('underline') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
> disabled={!editor.can().chain().focus().toggleUnderline().run()}
<Icon path={mdiFormatUnderline} className="size-5" /> >
</button> <Icon path={mdiFormatUnderline} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('static') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleStrike().run()} className={`btn btn-sm btn-square ${editor.isActive('static') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
> disabled={!editor.can().chain().focus().toggleStrike().run()}
<Icon path={mdiFormatStrikethrough} className="size-5" /> >
</button> <Icon path={mdiFormatStrikethrough} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('code') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleCode().run()} className={`btn btn-sm btn-square ${editor.isActive('code') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleCode().run()} onClick={() => editor.chain().focus().toggleCode().run()}
> disabled={!editor.can().chain().focus().toggleCode().run()}
<Icon path={mdiXml} className="size-5" /> >
</button> <Icon path={mdiXml} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('codeBlock') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleCodeBlock().run()} className={`btn btn-sm btn-square ${editor.isActive('codeBlock') ? 'btn-primary' : ''}`}
disabled={!editor.can().chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
> disabled={!editor.can().chain().focus().toggleCodeBlock().run()}
<Icon path={mdiCodeBracesBox} className="size-5" /> >
</button> <Icon path={mdiCodeBracesBox} className="size-5" />
<button </button>
type="button" <button
className={`btn btn-sm btn-square ${editor.isActive('blockquote') ? 'btn-primary' : ''}`} type="button"
onClick={() => editor.chain().focus().toggleBlockquote().run()} className={`btn btn-sm btn-square ${editor.isActive('blockquote') ? 'btn-primary' : ''}`}
> onClick={() => editor.chain().focus().toggleBlockquote().run()}
<Icon path={mdiFormatQuoteClose} className="size-5" /> >
</button> <Icon path={mdiFormatQuoteClose} className="size-5" />
</button>
</div>
</div> </div>
<EditorContent <EditorContent
editor={editor} editor={editor}
className="textarea textarea-bordered font-normal prose" className="textarea textarea-bordered font-normal prose max-w-full"
/> />
</div> </div>
) )

View File

@ -71,9 +71,9 @@ export const Success = ({ social }: CellSuccessProps<EditSocialById>) => {
} }
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table w-80"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>
<th className="w-0">Edit Social {social.id}</th> <th className="w-0">Edit Social {social.id}</th>

View File

@ -38,9 +38,9 @@ const NewSocial = () => {
createSocial({ variables: { input } }) createSocial({ variables: { input } })
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table w-80"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>
<th>New Social</th> <th>New Social</th>

View File

@ -44,31 +44,32 @@ const Social = ({ social }: Props) => {
} }
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="w-80"> <div className="w-full">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<th className="w-0"> <tr>
Social {social.id}: {social.name} <th colSpan={2}>
</th> Social {social.id}: {social.name}
<th>&nbsp;</th> </th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th>ID</th> <th className="text-right">ID</th>
<td>{social.id}</td> <td>{social.id}</td>
</tr> </tr>
<tr> <tr>
<th>Type</th> <th className="text-right">Type</th>
<td>{getLogoComponent(social.type)}</td> <td>{getLogoComponent(social.type)}</td>
</tr> </tr>
<tr> <tr>
<th>Name</th> <th className="text-right">Name</th>
<td>{social.name}</td> <td>{social.name}</td>
</tr> </tr>
<tr> <tr>
<th>Username</th> <th className="text-right">Username</th>
<td>{social.username}</td> <td>{social.username}</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -33,6 +33,6 @@ export const Failure = ({
export const Success = ({ export const Success = ({
social, social,
}: CellSuccessProps<FindSocialById, FindSocialByIdVariables>) => { }: CellSuccessProps<FindSocialById, FindSocialByIdVariables>) => (
return <Social social={social} /> <Social social={social} />
} )

View File

@ -99,7 +99,7 @@ const SocialForm = (props: SocialFormProps) => {
<Form<FormSocial> <Form<FormSocial>
onSubmit={onSubmit} onSubmit={onSubmit}
error={props.error} error={props.error}
className="h-128 max-w-80 space-y-2" className="h-128 space-y-2"
> >
<Label name="name" className="form-control w-full"> <Label name="name" className="form-control w-full">
<Label <Label

View File

@ -59,8 +59,8 @@ export const Success = ({ tag }: CellSuccessProps<EditTagById>) => {
updateTag({ variables: { id, input } }) updateTag({ variables: { id, input } })
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>

View File

@ -34,8 +34,8 @@ const NewTag = () => {
const onSave = (input: CreateTagInput) => createTag({ variables: { input } }) const onSave = (input: CreateTagInput) => createTag({ variables: { input } })
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
<table className="table"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
<tr> <tr>

View File

@ -42,8 +42,8 @@ const Tag = ({ tag }: Props) => {
} }
return ( return (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="w-80"> <div className="w-full">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table"> <table className="table">
<thead className="bg-base-200 font-syne"> <thead className="bg-base-200 font-syne">
@ -54,15 +54,15 @@ const Tag = ({ tag }: Props) => {
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th>ID</th> <th className="text-right">ID</th>
<td>{tag.id}</td> <td>{tag.id}</td>
</tr> </tr>
<tr> <tr>
<th>Tag</th> <th className="text-right">Tag</th>
<td>{tag.tag}</td> <td>{tag.tag}</td>
</tr> </tr>
<tr> <tr>
<th>Color</th> <th className="text-right">Color</th>
<td> <td>
<div <div
className="badge whitespace-nowrap" className="badge whitespace-nowrap"

View File

@ -35,7 +35,7 @@ const TagForm = (props: TagFormProps) => {
<Form<FormTag> <Form<FormTag>
onSubmit={onSubmit} onSubmit={onSubmit}
error={props.error} error={props.error}
className="max-w-56 space-y-2" className="space-y-2"
> >
<Label name="tag" className="form-control w-full"> <Label name="tag" className="form-control w-full">
<Label <Label

View File

@ -50,7 +50,7 @@ const TagsList = ({ tags }: FindTags) => {
<th className="w-0">&nbsp;</th> <th className="w-0">&nbsp;</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="overflow-y-scroll">
{tags.map((tag, i) => { {tags.map((tag, i) => {
const actionButtons = ( const actionButtons = (
<> <>

View File

@ -26,7 +26,7 @@ const TagsSelector = ({
}, [selectedTags, _tags]) }, [selectedTags, _tags])
return ( return (
<div className="w-80 space-y-2"> <div className="space-y-2">
{tags.length > 0 && ( {tags.length > 0 && (
<> <>
<p className="font-semibold">Tags</p> <p className="font-semibold">Tags</p>

View File

@ -29,22 +29,24 @@ export const Failure = ({
}: CellFailureProps<AdminTitlesQueryVariables>) => <CellFailure error={error} /> }: CellFailureProps<AdminTitlesQueryVariables>) => <CellFailure error={error} />
export const Success = ({ titles }: CellSuccessProps<AdminTitlesQuery>) => ( export const Success = ({ titles }: CellSuccessProps<AdminTitlesQuery>) => (
<div className="flex w-full justify-center"> <div className="flex mx-auto max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100"> <div className="w-full">
<table className="table w-80"> <div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<thead className="bg-base-200 font-syne"> <table className="table">
<tr> <thead className="bg-base-200 font-syne">
<th className="w-0">Titles</th> <tr>
</tr> <th className="w-0">Titles</th>
</thead> </tr>
<tbody> </thead>
<tr> <tbody>
<th> <tr>
<TitlesForm titles={titles} /> <th>
</th> <TitlesForm titles={titles} />
</tr> </th>
</tbody> </tr>
</table> </tbody>
</table>
</div>
</div> </div>
</div> </div>
) )

View File

@ -55,9 +55,9 @@ const TitlesForm = ({ titles }: TitlesFormProps) => {
}) })
return ( return (
<Form onSubmit={onSubmit} className="max-w-80 space-y-2"> <Form onSubmit={onSubmit} className="space-y-2">
<p className="text-center opacity-70"> <p className="text-center opacity-70">
The first one gets displayed for longer The first title gets displayed for longer
</p> </p>
{Array.from({ length: MAX_TITLES }).map((_, i) => ( {Array.from({ length: MAX_TITLES }).map((_, i) => (
<Label key={i} name={`title${i}`} className="form-control w-full"> <Label key={i} name={`title${i}`} className="form-control w-full">

View File

@ -1,3 +1,4 @@
import { hasFlag } from 'country-flag-icons'
import { hydrateRoot, createRoot } from 'react-dom/client' import { hydrateRoot, createRoot } from 'react-dom/client'
import App from 'src/App' import App from 'src/App'
@ -15,6 +16,11 @@ if (!redwoodAppElement)
"exists in your 'web/src/index.html' file." "exists in your 'web/src/index.html' file."
) )
if (!hasFlag(process.env.COUNTRY))
throw new Error(
'Invalid COUNTRY environment variable, please select a valid ISO-3166-1 alpha-2 country code\n See https://en.wikipedia.org/wiki/ISO_3166-1#Codes'
)
if (redwoodAppElement.children?.length > 0) if (redwoodAppElement.children?.length > 0)
hydrateRoot(redwoodAppElement, <App />) hydrateRoot(redwoodAppElement, <App />)
else { else {

View File

@ -23,3 +23,7 @@
.ProseMirror:focus { .ProseMirror:focus {
outline: none; outline: none;
} }
.w-full .react-colorful {
width: auto;
}

View File

@ -1,7 +1,5 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { Metadata } from '@redwoodjs/web'
import ContactCardCell from 'src/components/ContactCard/ContactCardCell' import ContactCardCell from 'src/components/ContactCard/ContactCardCell'
const ContactPage = () => { const ContactPage = () => {
@ -29,13 +27,9 @@ const ContactPage = () => {
}, [width, height]) }, [width, height])
return ( return (
<> <div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
<Metadata title="Contact" /> <ContactCardCell />
</div>
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
<ContactCardCell />
</div>
</>
) )
} }

View File

@ -1,5 +1,6 @@
import { mdiCompass, mdiContacts } from '@mdi/js' import { mdiCompass, mdiContacts } from '@mdi/js'
import Icon from '@mdi/react' import Icon from '@mdi/react'
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web' import { Metadata } from '@redwoodjs/web'
@ -9,7 +10,15 @@ import { getLogoComponent } from 'src/lib/handle'
const HomePage = () => ( const HomePage = () => (
<> <>
<Metadata title="Home" /> <Metadata
title="Home"
og={{
title: `${process.env.FIRST_NAME} ${process.env.LAST_NAME}`,
description: 'Check out my portfolio!',
type: 'website',
url: routes.home(),
}}
/>
<div className="hero min-h-[calc(100vh-6rem)]"> <div className="hero min-h-[calc(100vh-6rem)]">
<div className="hero-content flex flex-col gap-8"> <div className="hero-content flex flex-col gap-8">
@ -44,6 +53,11 @@ const HomePage = () => (
{getLogoComponent('gitea')} {getLogoComponent('gitea')}
</a> </a>
</div> </div>
<div className="fixed bottom-2 right-2 z-10">
<p className="btn btn-square text-xl">
{getUnicodeFlagIcon(process.env.COUNTRY)}
</p>
</div>
</> </>
) )

View File

@ -1,5 +1,3 @@
import { Metadata } from '@redwoodjs/web'
import ProjectCell from 'src/components/Project/ProjectCell' import ProjectCell from 'src/components/Project/ProjectCell'
interface ProjectPageProps { interface ProjectPageProps {
@ -7,13 +5,7 @@ interface ProjectPageProps {
} }
const ProjectPage = ({ id }: ProjectPageProps) => { const ProjectPage = ({ id }: ProjectPageProps) => {
return ( return <ProjectCell id={id} />
<>
<Metadata title="Project" />
<ProjectCell id={id} />
</>
)
} }
export default ProjectPage export default ProjectPage

View File

@ -1,14 +1,10 @@
import mobile from 'is-mobile' import mobile from 'is-mobile'
import { Metadata } from '@redwoodjs/web'
import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell' import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell'
const ProjectsPage = () => { const ProjectsPage = () => {
return ( return (
<> <>
<Metadata title="Projects" />
<div className="hero min-h-64"> <div className="hero min-h-64">
<div className="hero-content"> <div className="hero-content">
<div className="max-w-md text-center"> <div className="max-w-md text-center">

View File

@ -1,15 +1,7 @@
import { Metadata } from '@redwoodjs/web'
import ResumeCell from 'src/components/Resume/ResumeCell' import ResumeCell from 'src/components/Resume/ResumeCell'
const ResumePage = () => { const ResumePage = () => {
return ( return <ResumeCell />
<>
<Metadata title="Resume" />
<ResumeCell />
</>
)
} }
export default ResumePage export default ResumePage

View File

@ -7336,6 +7336,7 @@ __metadata:
"@tus/file-store": "npm:^1.4.0" "@tus/file-store": "npm:^1.4.0"
"@tus/server": "npm:^1.7.0" "@tus/server": "npm:^1.7.0"
"@types/nodemailer": "npm:^6.4.15" "@types/nodemailer": "npm:^6.4.15"
country-flag-icons: "npm:^1.5.13"
graphql-scalars: "npm:^1.23.0" graphql-scalars: "npm:^1.23.0"
nodemailer: "npm:^6.9.14" nodemailer: "npm:^6.9.14"
languageName: unknown languageName: unknown
@ -9061,6 +9062,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"country-flag-icons@npm:^1.5.13":
version: 1.5.13
resolution: "country-flag-icons@npm:1.5.13"
checksum: 10c0/beee2fe225469507d6c8df90376e031f08a5f103f65cd68e1db0679e82d4ffb2fbb27a3bb19defd112745b5c19d1972df615df21813c8c2074062dd5eb08eabb
languageName: node
linkType: hard
"crc-32@npm:^1.2.0": "crc-32@npm:^1.2.0":
version: 1.2.2 version: 1.2.2
resolution: "crc-32@npm:1.2.2" resolution: "crc-32@npm:1.2.2"
@ -19234,6 +19242,7 @@ __metadata:
"@uppy/tus": "npm:^4.0.0" "@uppy/tus": "npm:^4.0.0"
"@uppy/webcam": "npm:^4.0.1" "@uppy/webcam": "npm:^4.0.1"
autoprefixer: "npm:^10.4.20" autoprefixer: "npm:^10.4.20"
country-flag-icons: "npm:^1.5.13"
daisyui: "npm:^4.12.10" daisyui: "npm:^4.12.10"
date-fns: "npm:^4.1.0" date-fns: "npm:^4.1.0"
humanize-string: "npm:2.1.0" humanize-string: "npm:2.1.0"