Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
f03faabbee | |||
353fb3899e | |||
62ce137bcb | |||
f8987b08da | |||
cbf75acbeb |
@ -18,6 +18,8 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
|
||||
FIRST_NAME=firstname
|
||||
LAST_NAME=lastname
|
||||
|
||||
COUNTRY=US
|
||||
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
|
@ -6,6 +6,8 @@
|
||||
FIRST_NAME=firstname
|
||||
LAST_NAME=lastname
|
||||
|
||||
COUNTRY=US
|
||||
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
|
11
README.md
11
README.md
@ -21,9 +21,10 @@ services:
|
||||
- API_PROXY_TARGET=http://localhost:8911
|
||||
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||
- 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
|
||||
- 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_PORT=465
|
||||
- SMTP_SECURE=true
|
||||
@ -58,8 +59,6 @@ services:
|
||||
- POSTGRES_USER=redwood
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
- POSTGRES_DB=portfolio
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- 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
|
||||
- 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 correctly set up the Gmail app password, you should receive an email from yourself
|
||||
- It contains the link needed to change your password
|
||||
- If you correctly configured [SMTP](#smtp), you should receive an Email from [`EMAIL_FROM`](#docker-compose) to [`EMAIL_TO`](#docker-compose)
|
||||
- The Email contains the link needed to change your password
|
||||
### Default Credentials
|
||||
**Username:** `admin`
|
||||
|
||||
**Password:** [`SMTP_PASSWORD`](#smtp)
|
||||
**Password:** [`SMTP_PASSWORD`](#docker-compose)
|
||||
|
@ -11,6 +11,7 @@
|
||||
"@redwoodjs/graphql-server": "8.4.0",
|
||||
"@tus/file-store": "^1.4.0",
|
||||
"@tus/server": "^1.7.0",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"nodemailer": "^6.9.14"
|
||||
},
|
||||
|
@ -10,6 +10,13 @@ import { createServer } from '@redwoodjs/api-server'
|
||||
import { logger } from 'src/lib/logger'
|
||||
import { handleTusUpload } from 'src/lib/tus'
|
||||
;(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({
|
||||
logger,
|
||||
configureApiServer: async (server) => {
|
||||
|
@ -9,9 +9,10 @@ services:
|
||||
- API_PROXY_TARGET=http://localhost:8911
|
||||
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||
- 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
|
||||
- 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_PORT=465
|
||||
- SMTP_SECURE=true
|
||||
@ -46,8 +47,6 @@ services:
|
||||
- POSTGRES_USER=redwood
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
- POSTGRES_DB=portfolio
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
title = "${FIRST_NAME} ${LAST_NAME}"
|
||||
port = 8910
|
||||
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]
|
||||
tests = false
|
||||
stories = false
|
||||
|
@ -35,6 +35,7 @@
|
||||
"@uppy/react": "^4.0.1",
|
||||
"@uppy/tus": "^4.0.0",
|
||||
"@uppy/webcam": "^4.0.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"date-fns": "^4.1.0",
|
||||
"humanize-string": "2.1.0",
|
||||
"react": "18.3.1",
|
||||
|
@ -35,7 +35,7 @@ const Routes = () => {
|
||||
<Route path="/admin/resume" page={ResumeAdminResumePage} name="adminResume" />
|
||||
</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/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" />
|
||||
<Route path="/admin/projects/{id:Int}" page={ProjectAdminProjectPage} name="adminProject" />
|
||||
|
@ -9,15 +9,12 @@ interface ColorPickerProps {
|
||||
setColor: React.Dispatch<React.SetStateAction<string>>
|
||||
}
|
||||
|
||||
const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2 bg-base-100 p-2 rounded-xl w-min">
|
||||
<section className="w-52">
|
||||
const ColorPicker = ({ color, setColor }: ColorPickerProps) => (
|
||||
<div className="flex flex-col space-y-2 bg-base-100 p-2 rounded-xl">
|
||||
<HexColorPicker color={color} onChange={setColor} />
|
||||
</section>
|
||||
<div className="flex space-x-2 w-52">
|
||||
<div className="flex space-x-2">
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label className="input input-bordered flex items-center gap-2 input-sm w-full grow">
|
||||
<label className="input input-bordered flex items-center gap-2 input-sm grow">
|
||||
<Icon path={mdiPound} className="size-4 opacity-70" />
|
||||
<HexColorInput color={color} className="w-16" />
|
||||
</label>
|
||||
@ -37,7 +34,7 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-square btn-sm "
|
||||
className="btn btn-square btn-sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText()
|
||||
@ -56,7 +53,6 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ColorPicker
|
||||
|
@ -3,10 +3,12 @@ import type {
|
||||
ContactCardPortraitVariables,
|
||||
} from 'types/graphql'
|
||||
|
||||
import type {
|
||||
TypedDocumentNode,
|
||||
CellFailureProps,
|
||||
CellSuccessProps,
|
||||
import { routes } from '@redwoodjs/router'
|
||||
import {
|
||||
type TypedDocumentNode,
|
||||
type CellFailureProps,
|
||||
type CellSuccessProps,
|
||||
Metadata,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
|
||||
@ -43,5 +45,20 @@ export const Success = ({
|
||||
portrait,
|
||||
socials,
|
||||
}: CellSuccessProps<ContactCardPortrait, ContactCardPortraitVariables>) => (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
|
@ -1,19 +1,37 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { mdiAlertOutline } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
interface PDFProps {
|
||||
url: string
|
||||
form?: boolean
|
||||
}
|
||||
|
||||
const PDF = ({ url, form = false }: PDFProps) => (
|
||||
<embed
|
||||
const PDF = ({ url, form = false }: PDFProps) => {
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
|
||||
return error ? (
|
||||
<div role="alert" className="alert alert-warning">
|
||||
<Icon path={mdiAlertOutline} className="size-7" />
|
||||
<span>
|
||||
Could not load PDF, this is common in in-app browsers, try opening this
|
||||
page in a regular browser
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
src={url}
|
||||
title="PDF"
|
||||
type="application/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
|
||||
|
@ -127,14 +127,13 @@ const PortraitForm = ({ portrait }: PortraitFormProps) => {
|
||||
)
|
||||
else
|
||||
return (
|
||||
<div className="mx-auto w-fit space-y-2">
|
||||
<div className="mx-auto max-w-prose space-y-2">
|
||||
{!fileId ? (
|
||||
<>
|
||||
<Uploader
|
||||
onComplete={onUploadComplete}
|
||||
width="22rem"
|
||||
height="34.5rem"
|
||||
className="flex justify-center"
|
||||
width="auto"
|
||||
height="30rem"
|
||||
/>
|
||||
<p className="text-center">
|
||||
High quality, 4:5 aspect ratio image recommended
|
||||
|
@ -46,29 +46,28 @@ const AdminProject = ({ project }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="flex justify-center">
|
||||
<div>
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th className="w-0">
|
||||
<th colSpan={2}>
|
||||
Project {project.id}: {project.title}
|
||||
</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th className="text-right">ID</th>
|
||||
<td>{project.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th className="text-right">Title</th>
|
||||
<td>{project.title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th className="text-right">Description</th>
|
||||
<td>
|
||||
<article className="prose">
|
||||
{parseHtml(project.description)}
|
||||
@ -76,11 +75,11 @@ const AdminProject = ({ project }: Props) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th className="text-right">Date</th>
|
||||
<td>{timeTag(project.date)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Images</th>
|
||||
<th className="text-right">Images</th>
|
||||
<td>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.images.map((image, i) => (
|
||||
@ -98,7 +97,7 @@ const AdminProject = ({ project }: Props) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tags</th>
|
||||
<th className="text-right">Tags</th>
|
||||
<td>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag, i) => (
|
||||
@ -120,19 +119,30 @@ const AdminProject = ({ project }: Props) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Links</th>
|
||||
<th className="text-right">Links</th>
|
||||
<td>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.links.map((link, i) => (
|
||||
<>
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="badge badge-ghost text-nowrap"
|
||||
className="hidden sm:flex badge badge-ghost text-nowrap"
|
||||
key={i}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="btn btn-sm btn-square sm:hidden"
|
||||
key={i}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{i + 1}
|
||||
</a>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
|
@ -73,12 +73,12 @@ export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
|
||||
) => updateProject({ variables: { id, input } })
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<div className="flex mx-auto max-w-prose justify-center">
|
||||
<div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th className="w-0">Edit Project {project.id}</th>
|
||||
<th>Edit Project {project.id}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -38,9 +38,9 @@ const NewProject = () => {
|
||||
createProject({ variables: { input } })
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<div className="flex mx-auto max-w-prose justify-center">
|
||||
<div className="overflow-hidden w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th>New Project</th>
|
||||
|
@ -69,10 +69,13 @@ const Project = ({ project }: Props) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{project.images.length > 0 && (
|
||||
<h2 className="sm:hidden font-bold text-3xl text-center">Images</h2>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 items-center pt-8 justify-center">
|
||||
{project.images.map((image, i) => (
|
||||
<div className="flex flex-wrap gap-4 pt-8 justify-center h-fit">
|
||||
{project.images.length > 0 &&
|
||||
project.images.map((image, i) => (
|
||||
<a
|
||||
href={image}
|
||||
target="_blank"
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { FindProjectById, FindProjectByIdVariables } from 'types/graphql'
|
||||
|
||||
import type {
|
||||
CellSuccessProps,
|
||||
CellFailureProps,
|
||||
TypedDocumentNode,
|
||||
import { routes } from '@redwoodjs/router'
|
||||
import {
|
||||
type CellSuccessProps,
|
||||
type CellFailureProps,
|
||||
type TypedDocumentNode,
|
||||
Metadata,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
|
||||
@ -40,5 +42,23 @@ export const Failure = ({
|
||||
export const Success = ({
|
||||
project,
|
||||
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
|
@ -119,7 +119,7 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
<Form<FormProject>
|
||||
onSubmit={onSubmit}
|
||||
error={props.error}
|
||||
className="space-y-2 w-80"
|
||||
className="space-y-2"
|
||||
>
|
||||
<Label name="title" className="form-control w-full">
|
||||
<Label
|
||||
@ -261,9 +261,8 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
{appendUploader && (
|
||||
<Uploader
|
||||
onComplete={onUploadComplete}
|
||||
width="20rem"
|
||||
width="auto"
|
||||
height="30rem"
|
||||
className="flex justify-center"
|
||||
maxFiles={10}
|
||||
disabled={props.loading}
|
||||
/>
|
||||
@ -272,9 +271,8 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
) : (
|
||||
<Uploader
|
||||
onComplete={onUploadComplete}
|
||||
width="20rem"
|
||||
width="auto"
|
||||
height="30rem"
|
||||
className="flex justify-center pt-3"
|
||||
maxFiles={10}
|
||||
disabled={props.loading}
|
||||
/>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
|
||||
|
||||
import type {
|
||||
CellSuccessProps,
|
||||
CellFailureProps,
|
||||
TypedDocumentNode,
|
||||
import { routes } from '@redwoodjs/router'
|
||||
import {
|
||||
type CellSuccessProps,
|
||||
type CellFailureProps,
|
||||
type TypedDocumentNode,
|
||||
Metadata,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
|
||||
@ -40,5 +42,16 @@ export const Failure = ({ error }: CellFailureProps<FindProjectsVariables>) => (
|
||||
export const Success = ({
|
||||
projects,
|
||||
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
|
||||
<>
|
||||
<Metadata
|
||||
title="Projects"
|
||||
og={{
|
||||
title: 'Projects',
|
||||
type: 'website',
|
||||
description: `${projects.length} projects`,
|
||||
url: routes.projects(),
|
||||
}}
|
||||
/>
|
||||
<ProjectsShowcase projects={projects} />
|
||||
</>
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { FindResume, FindResumeVariables } from 'types/graphql'
|
||||
|
||||
import type {
|
||||
CellSuccessProps,
|
||||
CellFailureProps,
|
||||
TypedDocumentNode,
|
||||
import { routes } from '@redwoodjs/router'
|
||||
import {
|
||||
type CellSuccessProps,
|
||||
type CellFailureProps,
|
||||
type TypedDocumentNode,
|
||||
Metadata,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
|
||||
@ -29,5 +31,15 @@ export const Failure = ({ error }: CellFailureProps<FindResumeVariables>) => (
|
||||
export const Success = ({
|
||||
resume,
|
||||
}: CellSuccessProps<FindResume, FindResumeVariables>) => (
|
||||
<>
|
||||
<Metadata
|
||||
title="Contact"
|
||||
og={{
|
||||
title: 'Resume',
|
||||
type: 'website',
|
||||
url: routes.resume(),
|
||||
}}
|
||||
/>
|
||||
<Resume resume={resume} />
|
||||
</>
|
||||
)
|
||||
|
@ -123,14 +123,13 @@ const ResumeForm = ({ resume }: ResumeFormProps) => {
|
||||
)
|
||||
else
|
||||
return (
|
||||
<div className="mx-auto w-fit space-y-2">
|
||||
<div className="mx-auto max-w-prose space-y-2">
|
||||
{!fileId ? (
|
||||
<>
|
||||
<Uploader
|
||||
onComplete={onUploadComplete}
|
||||
width="22rem"
|
||||
height="11.5rem"
|
||||
className="flex justify-center"
|
||||
width="auto"
|
||||
height="30rem"
|
||||
type="pdf"
|
||||
/>
|
||||
</>
|
||||
|
@ -43,7 +43,8 @@ const RichTextEditor = ({ editor }: RichTextEditorProps) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="flex gap-2 h-min justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm btn-square ${editor.isActive('link') ? 'btn-primary' : ''}`}
|
||||
@ -97,7 +98,7 @@ const RichTextEditor = ({ editor }: RichTextEditorProps) => {
|
||||
<Icon path={mdiRedoVariant} className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<div className="flex gap-2 h-min justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm btn-square ${editor.isActive('bold') ? 'btn-primary' : ''}`}
|
||||
@ -154,9 +155,10 @@ const RichTextEditor = ({ editor }: RichTextEditorProps) => {
|
||||
<Icon path={mdiFormatQuoteClose} className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className="textarea textarea-bordered font-normal prose"
|
||||
className="textarea textarea-bordered font-normal prose max-w-full"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -71,9 +71,9 @@ export const Success = ({ social }: CellSuccessProps<EditSocialById>) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<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 w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th className="w-0">Edit Social {social.id}</th>
|
||||
|
@ -38,9 +38,9 @@ const NewSocial = () => {
|
||||
createSocial({ variables: { input } })
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<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 w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th>New Social</th>
|
||||
|
@ -44,31 +44,32 @@ const Social = ({ social }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-80">
|
||||
<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-full">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<th className="w-0">
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
Social {social.id}: {social.name}
|
||||
</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th className="text-right">ID</th>
|
||||
<td>{social.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th className="text-right">Type</th>
|
||||
<td>{getLogoComponent(social.type)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th className="text-right">Name</th>
|
||||
<td>{social.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th className="text-right">Username</th>
|
||||
<td>{social.username}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -33,6 +33,6 @@ export const Failure = ({
|
||||
|
||||
export const Success = ({
|
||||
social,
|
||||
}: CellSuccessProps<FindSocialById, FindSocialByIdVariables>) => {
|
||||
return <Social social={social} />
|
||||
}
|
||||
}: CellSuccessProps<FindSocialById, FindSocialByIdVariables>) => (
|
||||
<Social social={social} />
|
||||
)
|
||||
|
@ -99,7 +99,7 @@ const SocialForm = (props: SocialFormProps) => {
|
||||
<Form<FormSocial>
|
||||
onSubmit={onSubmit}
|
||||
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
|
||||
|
@ -59,8 +59,8 @@ export const Success = ({ tag }: CellSuccessProps<EditTagById>) => {
|
||||
updateTag({ variables: { id, input } })
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<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 w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
|
@ -34,8 +34,8 @@ const NewTag = () => {
|
||||
const onSave = (input: CreateTagInput) => createTag({ variables: { input } })
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<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 w-full overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
|
@ -42,8 +42,8 @@ const Tag = ({ tag }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-80">
|
||||
<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-full">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
@ -54,15 +54,15 @@ const Tag = ({ tag }: Props) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th className="text-right">ID</th>
|
||||
<td>{tag.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th className="text-right">Tag</th>
|
||||
<td>{tag.tag}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Color</th>
|
||||
<th className="text-right">Color</th>
|
||||
<td>
|
||||
<div
|
||||
className="badge whitespace-nowrap"
|
||||
|
@ -35,7 +35,7 @@ const TagForm = (props: TagFormProps) => {
|
||||
<Form<FormTag>
|
||||
onSubmit={onSubmit}
|
||||
error={props.error}
|
||||
className="max-w-56 space-y-2"
|
||||
className="space-y-2"
|
||||
>
|
||||
<Label name="tag" className="form-control w-full">
|
||||
<Label
|
||||
|
@ -50,7 +50,7 @@ const TagsList = ({ tags }: FindTags) => {
|
||||
<th className="w-0"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="overflow-y-scroll">
|
||||
{tags.map((tag, i) => {
|
||||
const actionButtons = (
|
||||
<>
|
||||
|
@ -26,7 +26,7 @@ const TagsSelector = ({
|
||||
}, [selectedTags, _tags])
|
||||
|
||||
return (
|
||||
<div className="w-80 space-y-2">
|
||||
<div className="space-y-2">
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
<p className="font-semibold">Tags</p>
|
||||
|
@ -29,9 +29,10 @@ export const Failure = ({
|
||||
}: CellFailureProps<AdminTitlesQueryVariables>) => <CellFailure error={error} />
|
||||
|
||||
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="w-full">
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<table className="table">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<tr>
|
||||
<th className="w-0">Titles</th>
|
||||
@ -47,4 +48,5 @@ export const Success = ({ titles }: CellSuccessProps<AdminTitlesQuery>) => (
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -55,9 +55,9 @@ const TitlesForm = ({ titles }: TitlesFormProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Form onSubmit={onSubmit} className="max-w-80 space-y-2">
|
||||
<Form onSubmit={onSubmit} className="space-y-2">
|
||||
<p className="text-center opacity-70">
|
||||
The first one gets displayed for longer
|
||||
The first title gets displayed for longer
|
||||
</p>
|
||||
{Array.from({ length: MAX_TITLES }).map((_, i) => (
|
||||
<Label key={i} name={`title${i}`} className="form-control w-full">
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { hasFlag } from 'country-flag-icons'
|
||||
import { hydrateRoot, createRoot } from 'react-dom/client'
|
||||
|
||||
import App from 'src/App'
|
||||
@ -15,6 +16,11 @@ if (!redwoodAppElement)
|
||||
"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)
|
||||
hydrateRoot(redwoodAppElement, <App />)
|
||||
else {
|
||||
|
@ -23,3 +23,7 @@
|
||||
.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.w-full .react-colorful {
|
||||
width: auto;
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
import ContactCardCell from 'src/components/ContactCard/ContactCardCell'
|
||||
|
||||
const ContactPage = () => {
|
||||
@ -29,13 +27,9 @@ const ContactPage = () => {
|
||||
}, [width, height])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Metadata title="Contact" />
|
||||
|
||||
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
|
||||
<ContactCardCell />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { mdiCompass, mdiContacts } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
@ -9,7 +10,15 @@ import { getLogoComponent } from 'src/lib/handle'
|
||||
|
||||
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-content flex flex-col gap-8">
|
||||
@ -44,6 +53,11 @@ const HomePage = () => (
|
||||
{getLogoComponent('gitea')}
|
||||
</a>
|
||||
</div>
|
||||
<div className="fixed bottom-2 right-2 z-10">
|
||||
<p className="btn btn-square text-xl">
|
||||
{getUnicodeFlagIcon(process.env.COUNTRY)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
import ProjectCell from 'src/components/Project/ProjectCell'
|
||||
|
||||
interface ProjectPageProps {
|
||||
@ -7,13 +5,7 @@ interface ProjectPageProps {
|
||||
}
|
||||
|
||||
const ProjectPage = ({ id }: ProjectPageProps) => {
|
||||
return (
|
||||
<>
|
||||
<Metadata title="Project" />
|
||||
|
||||
<ProjectCell id={id} />
|
||||
</>
|
||||
)
|
||||
return <ProjectCell id={id} />
|
||||
}
|
||||
|
||||
export default ProjectPage
|
||||
|
@ -1,14 +1,10 @@
|
||||
import mobile from 'is-mobile'
|
||||
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell'
|
||||
|
||||
const ProjectsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Metadata title="Projects" />
|
||||
|
||||
<div className="hero min-h-64">
|
||||
<div className="hero-content">
|
||||
<div className="max-w-md text-center">
|
||||
|
@ -1,15 +1,7 @@
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
import ResumeCell from 'src/components/Resume/ResumeCell'
|
||||
|
||||
const ResumePage = () => {
|
||||
return (
|
||||
<>
|
||||
<Metadata title="Resume" />
|
||||
|
||||
<ResumeCell />
|
||||
</>
|
||||
)
|
||||
return <ResumeCell />
|
||||
}
|
||||
|
||||
export default ResumePage
|
||||
|
@ -7336,6 +7336,7 @@ __metadata:
|
||||
"@tus/file-store": "npm:^1.4.0"
|
||||
"@tus/server": "npm:^1.7.0"
|
||||
"@types/nodemailer": "npm:^6.4.15"
|
||||
country-flag-icons: "npm:^1.5.13"
|
||||
graphql-scalars: "npm:^1.23.0"
|
||||
nodemailer: "npm:^6.9.14"
|
||||
languageName: unknown
|
||||
@ -9061,6 +9062,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.2.2
|
||||
resolution: "crc-32@npm:1.2.2"
|
||||
@ -19234,6 +19242,7 @@ __metadata:
|
||||
"@uppy/tus": "npm:^4.0.0"
|
||||
"@uppy/webcam": "npm:^4.0.1"
|
||||
autoprefixer: "npm:^10.4.20"
|
||||
country-flag-icons: "npm:^1.5.13"
|
||||
daisyui: "npm:^4.12.10"
|
||||
date-fns: "npm:^4.1.0"
|
||||
humanize-string: "npm:2.1.0"
|
||||
|
Reference in New Issue
Block a user