8 Commits

Author SHA1 Message Date
f14732cdf0 Upgrade RedwoodJS
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 1m6s
2024-10-15 14:52:01 -04:00
77db153fe6 Add Matrix social 2024-10-15 14:51:43 -04:00
684d6f88c2 Combine GraphQL queries for contact card (2 -> 1) 2024-10-15 14:30:05 -04:00
03f606bbde Improve color picker paste logic 2024-10-15 14:21:52 -04:00
1eafaee2c0 Follow system color scheme by default instead of light 2024-10-10 14:09:09 -04:00
b8063e8692 Auth tweaks
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 26s
2024-10-09 20:44:12 -04:00
738260f7de Watermark 2024-10-09 20:37:27 -04:00
82313bef46 Simplify PDF embed 2024-10-09 20:32:39 -04:00
28 changed files with 651 additions and 428 deletions

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Handle" ADD VALUE 'matrix';

View File

@ -25,6 +25,7 @@ enum Handle {
discord
twitch
linkedin
matrix
github
gitea
forgejo

View File

@ -5,10 +5,10 @@
"dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/rate-limit": "^9.1.0",
"@redwoodjs/api": "8.3.0",
"@redwoodjs/api-server": "8.3.0",
"@redwoodjs/auth-dbauth-api": "8.3.0",
"@redwoodjs/graphql-server": "8.3.0",
"@redwoodjs/api": "8.4.0",
"@redwoodjs/api-server": "8.4.0",
"@redwoodjs/auth-dbauth-api": "8.4.0",
"@redwoodjs/graphql-server": "8.4.0",
"@tus/file-store": "^1.4.0",
"@tus/server": "^1.7.0",
"graphql-scalars": "^1.23.0",

View File

@ -111,9 +111,7 @@ ${domain}/reset-password?resetToken=${resetToken}
// the database. Returning anything truthy will automatically log the user
// in. Return `false` otherwise, and in the Reset Password page redirect the
// user to the login page.
handler: (_user) => {
return true
},
handler: (_user) => false,
// If `false` then the new password MUST be different from the current one
allowReusedPassword: true,

View File

@ -17,6 +17,7 @@ export const schema = gql`
discord
twitch
linkedin
matrix
github
gitea
forgejo

View File

@ -15,15 +15,14 @@ const transporter = nodemailer.createTransport({
},
})
export const sendEmail = async ({ to, subject, text, html }: Options) => {
return await transporter.sendMail({
export const sendEmail = async ({ to, subject, text, html }: Options) =>
await transporter.sendMail({
from: `"${process.env.FIRST_NAME} ${process.env.LAST_NAME} (noreply)" <${process.env.GMAIL}>`,
to: Array.isArray(to) ? to : [to],
subject,
text,
html,
})
}
export const censorEmail = (email: string): string => {
const [localPart, domain] = email.split('@')

View File

@ -7,9 +7,9 @@
]
},
"devDependencies": {
"@redwoodjs/auth-dbauth-setup": "8.3.0",
"@redwoodjs/core": "8.3.0",
"@redwoodjs/project-config": "8.3.0",
"@redwoodjs/auth-dbauth-setup": "8.4.0",
"@redwoodjs/core": "8.4.0",
"@redwoodjs/project-config": "8.4.0",
"prettier-plugin-tailwindcss": "0.4.1"
},
"eslintConfig": {

View File

@ -6,6 +6,7 @@ import {
SiTiktokHex,
SiYoutubeHex,
SiLinkedinHex,
SiMatrixHex,
SiGithubHex,
SiGiteaHex,
SiLeetcodeHex,
@ -112,6 +113,10 @@ export const theme = {
light: SiLinkedinHex,
dark: SiLinkedinHex,
},
matrix: {
light: SiMatrixHex,
dark: invertColor(SiMatrixHex),
},
github: {
light: SiGithubHex,
dark: invertColor(SiGithubHex),

View File

@ -14,11 +14,11 @@
"@icons-pack/react-simple-icons": "^10.0.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@redwoodjs/auth-dbauth-web": "8.3.0",
"@redwoodjs/forms": "8.3.0",
"@redwoodjs/router": "8.3.0",
"@redwoodjs/web": "8.3.0",
"@redwoodjs/web-server": "8.3.0",
"@redwoodjs/auth-dbauth-web": "8.4.0",
"@redwoodjs/forms": "8.4.0",
"@redwoodjs/router": "8.4.0",
"@redwoodjs/web": "8.4.0",
"@redwoodjs/web-server": "8.4.0",
"@tailwindcss/typography": "^0.5.15",
"@tiptap/extension-link": "^2.8.0",
"@tiptap/extension-text-style": "^2.8.0",
@ -44,7 +44,7 @@
"react-typed": "^2.0.12"
},
"devDependencies": {
"@redwoodjs/vite": "8.3.0",
"@redwoodjs/vite": "8.4.0",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-html-parser": "^2",

View File

@ -40,7 +40,13 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
className="btn btn-square btn-sm "
onClick={async () => {
try {
setColor(await navigator.clipboard.readText())
const clipboardText = await navigator.clipboard.readText()
const hexColorRegex =
/^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i
if (!hexColorRegex.test(clipboardText))
toast.error(`Text is not a valid hex color`)
else setColor(clipboardText)
} catch {
toast.error(`Failed to paste, please try again`)
}

View File

@ -1,12 +1,15 @@
import { useState, useRef, useLayoutEffect } from 'react'
import SocialLinksCell from 'src/components/Social/SocialLinksCell'
import { ContactCardPortrait } from 'types/graphql'
import SocialLinks from 'src/components/Social/SocialLinks/SocialLinks'
interface ContactCardProps {
portraitUrl: string
socials: ContactCardPortrait['socials']
}
const ContactCard = ({ portraitUrl }: ContactCardProps) => {
const ContactCard = ({ portraitUrl, socials }: ContactCardProps) => {
const [width, setWidth] = useState()
const [height, setHeight] = useState()
@ -68,7 +71,7 @@ const ContactCard = ({ portraitUrl }: ContactCardProps) => {
</h2>
<p className="p-2"></p>
<div className="card-actions">
<SocialLinksCell />
<SocialLinks socials={socials} />
</div>
</div>
</div>

View File

@ -1,4 +1,7 @@
import type { FindPortrait, FindPortraitVariables } from 'types/graphql'
import type {
ContactCardPortrait,
ContactCardPortraitVariables,
} from 'types/graphql'
import type {
TypedDocumentNode,
@ -11,25 +14,34 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import ContactCard from 'src/components/ContactCard/ContactCard/ContactCard'
export const QUERY: TypedDocumentNode<FindPortrait, FindPortraitVariables> =
gql`
export const QUERY: TypedDocumentNode<
ContactCardPortrait,
ContactCardPortraitVariables
> = gql`
query ContactCardPortrait {
portrait: portrait {
portrait {
fileId
}
socials {
id
name
type
username
}
`
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindPortrait>) => (
export const Failure = ({ error }: CellFailureProps<ContactCardPortrait>) => (
<CellFailure error={error} />
)
export const Success = ({
portrait,
}: CellSuccessProps<FindPortrait, FindPortraitVariables>) => (
<ContactCard portraitUrl={portrait.fileId} />
socials,
}: CellSuccessProps<ContactCardPortrait, ContactCardPortraitVariables>) => (
<ContactCard portraitUrl={portrait.fileId} socials={socials} />
)

View File

@ -0,0 +1,19 @@
interface PDFProps {
url: string
form?: boolean
}
const PDF = ({ url, form = false }: PDFProps) => (
<embed
src={url}
title="PDF"
type="application/pdf"
style={{
width: 'calc(100vw - 1rem)',
height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`,
}}
className="rounded-xl"
/>
)
export default PDF

View File

@ -102,7 +102,7 @@ const PortraitForm = ({ portrait }: PortraitFormProps) => {
return (
<div className="mx-auto w-fit space-y-2">
<img
className="aspect-portrait max-w-2xl rounded-xl object-cover"
className="aspect-portrait max-w-80 sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl rounded-xl object-cover"
src={portrait?.fileId}
alt={`${process.env.FIRST_NAME} Portrait`}
/>

View File

@ -1,26 +1,11 @@
import { useState } from 'react'
import { Resume as ResumeType } from 'types/graphql'
import PDF from 'src/components/PDF/PDF'
interface ResumeProps {
resume?: ResumeType
}
const Resume = ({ resume }: ResumeProps) => {
const [fileId] = useState<string>(resume?.fileId)
return (
<object
data={fileId}
type="application/pdf"
aria-label="Resume PDF"
style={{
width: 'calc(100vw - 1rem)',
height: 'calc(100vh - 6rem)',
}}
className="rounded-xl"
/>
)
}
const Resume = ({ resume }: ResumeProps) => <PDF url={resume?.fileId} />
export default Resume

View File

@ -14,6 +14,7 @@ import type {
import { TypedDocumentNode, useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import PDF from 'src/components/PDF/PDF'
import Uploader from 'src/components/Uploader/Uploader'
import { deleteFile, handleBeforeUnload } from 'src/lib/tus'
@ -100,16 +101,7 @@ const ResumeForm = ({ resume }: ResumeFormProps) => {
if (resume?.fileId)
return (
<div className="mx-auto w-fit space-y-2">
<object
data={resume?.fileId}
type="application/pdf"
aria-label="Resume PDF"
style={{
width: 'calc(100vw - 1rem)',
height: 'calc(100vh - 10rem)',
}}
className="rounded-xl"
/>
<PDF form url={resume?.fileId} />
<div className="flex justify-center">
<button
type="button"
@ -143,16 +135,7 @@ const ResumeForm = ({ resume }: ResumeFormProps) => {
/>
</>
) : (
<object
data={fileId}
type="application/pdf"
aria-label="Resume PDF"
style={{
width: 'calc(100vw - 1rem)',
height: 'calc(100vh - 10rem)',
}}
className="rounded-xl"
/>
<PDF form url={fileId} />
)}
{fileId && (
<div className="flex justify-center space-x-2">

View File

@ -38,6 +38,7 @@ const types: FormSocial['type'][] = [
'discord',
'twitch',
'linkedin',
'matrix',
'github',
'gitea',
'forgejo',
@ -52,6 +53,15 @@ const types: FormSocial['type'][] = [
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'
)
@ -177,17 +187,20 @@ const SocialForm = (props: SocialFormProps) => {
pattern: {
value:
type == 'email'
? /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
? emailRegex
: type == 'phone'
? /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
: urlHandles.includes(type) &&
/^(?:(?:(?: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,
? phoneRegex
: urlHandles.includes(type)
? urlRegex
: type == 'matrix' && matrixRegex,
message: `Invalid ${
urlHandles.includes(type)
? 'URL'
: type == 'phone'
? 'Phone Number'
: type == 'email' && 'Email'
? 'phone number'
: type == 'email'
? 'Email'
: type == 'matrix' && 'Matrix identifier'
}`,
},
}}

View File

@ -1,8 +1,8 @@
import { FindSocials } from 'types/graphql'
import { ContactCardPortrait } from 'types/graphql'
import { baseUrls, getLogoComponent, sortOrder } from 'src/lib/handle'
const SocialLinks = ({ socials }: FindSocials) => (
const SocialLinks = ({ socials }: ContactCardPortrait) => (
<div className={`grid max-w-68 grid-cols-[repeat(auto-fit,3rem)] gap-2`}>
{[...socials]
.sort((a, b) => sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type))

View File

@ -1,35 +0,0 @@
import type { FindSocials, FindSocialsVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import SocialLinks from 'src/components/Social/SocialLinks/SocialLinks'
export const QUERY: TypedDocumentNode<FindSocials, FindSocialsVariables> = gql`
query SocialsQuery {
socials {
id
name
type
username
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindSocials>) => (
<CellFailure error={error} />
)
export const Success = ({ socials }: CellSuccessProps<FindSocials>) => (
<SocialLinks socials={socials} />
)

View File

@ -8,7 +8,10 @@ const DARK_THEME = 'dark'
const ThemeToggle = () => {
const [theme, setTheme] = useState(
localStorage.getItem('theme') ?? LIGHT_THEME
(localStorage.getItem('theme') ??
window.matchMedia('(prefers-color-scheme: dark)').matches)
? DARK_THEME
: LIGHT_THEME
)
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -97,7 +97,7 @@ const TitlesForm = ({ titles }: TitlesFormProps) => {
<nav className="my-2 flex justify-center space-x-2">
<button
type="button"
className="btn btn-sm"
className="btn btn-sm uppercase"
onClick={() => setPreview(!preview)}
>
{preview ? 'Hide' : 'Show'} Preview

View File

@ -8,6 +8,7 @@ import {
SiTiktok,
SiYoutube,
SiLinkedin,
SiMatrix,
SiGithub,
SiGitea,
SiLeetcode,
@ -33,6 +34,7 @@ export const baseUrls: Record<Handle, string> = {
discord: 'https://discord.gg/',
twitch: 'https://www.twitch.tv/',
linkedin: 'https://www.linkedin.com/in/',
matrix: 'https://matrix.to/#/',
github: 'https://github.com/',
gitea: '',
forgejo: '',
@ -61,6 +63,7 @@ const logoComponents: Record<Handle, ReactElement> = {
linkedin: (
<SiLinkedin className="text-linkedin-light dark:text-linkedin-dark" />
),
matrix: <SiMatrix className="text-matrix-light dark:text-matrix-dark" />,
github: <SiGithub className="text-github-light dark:text-github-dark" />,
gitea: <SiGitea className="text-gitea-light dark:text-gitea-dark" />,
forgejo: <SiForgejo className="text-forgejo-light dark:text-forgejo-dark" />,
@ -80,6 +83,7 @@ export const sortOrder: Social['type'][] = [
'phone',
'email',
'custom',
'matrix',
'linkedin',
'leetcode',
'github',

View File

@ -13,45 +13,6 @@ import { DevFatalErrorPage } from '@redwoodjs/web/dist/components/DevFatalErrorP
export default DevFatalErrorPage ||
(() => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>Something went wrong</span>
</h1>
</section>
</main>
))

View File

@ -72,7 +72,9 @@ const ForgotPasswordPage = () => {
<FieldError name="username" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary btn-sm mx-auto">Submit</Submit>
<Submit className="btn btn-primary btn-sm mx-auto uppercase">
Submit
</Submit>
</div>
</Form>
</div>

View File

@ -5,6 +5,7 @@ import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import TitlesCell from 'src/components/Title/TitlesCell'
import { getLogoComponent } from 'src/lib/handle'
const HomePage = () => (
<>
@ -33,6 +34,16 @@ const HomePage = () => (
</div>
</div>
</div>
<div className="fixed bottom-2 left-2 z-10">
<a
href="https://git.altaiar.dev/ahmed/portfolio"
target="_blank"
rel="noreferrer"
className="btn btn-square"
>
{getLogoComponent('gitea')}
</a>
</div>
</>
)

View File

@ -103,7 +103,9 @@ const LoginPage = () => {
<FieldError name="password" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary btn-sm mx-auto">Log In</Submit>
<Submit className="btn btn-primary btn-sm mx-auto uppercase">
Log In
</Submit>
</div>
</Form>
</div>

View File

@ -102,7 +102,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
<div className="flex w-full">
<Submit
className={`btn btn-primary btn-sm mx-auto ${
className={`btn btn-primary btn-sm uppercase mx-auto ${
!enabled ? 'btn-disabled' : ''
}`}
disabled={!enabled}

782
yarn.lock

File diff suppressed because it is too large Load Diff