Compare commits
5 Commits
v1.0.2
..
f14732cdf0
| Author | SHA1 | Date | |
|---|---|---|---|
| f14732cdf0 | |||
| 77db153fe6 | |||
| 684d6f88c2 | |||
| 03f606bbde | |||
| 1eafaee2c0 |
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'matrix';
|
||||||
@@ -25,6 +25,7 @@ enum Handle {
|
|||||||
discord
|
discord
|
||||||
twitch
|
twitch
|
||||||
linkedin
|
linkedin
|
||||||
|
matrix
|
||||||
github
|
github
|
||||||
gitea
|
gitea
|
||||||
forgejo
|
forgejo
|
||||||
|
|||||||
+4
-4
@@ -5,10 +5,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^9.0.1",
|
"@fastify/cors": "^9.0.1",
|
||||||
"@fastify/rate-limit": "^9.1.0",
|
"@fastify/rate-limit": "^9.1.0",
|
||||||
"@redwoodjs/api": "8.3.0",
|
"@redwoodjs/api": "8.4.0",
|
||||||
"@redwoodjs/api-server": "8.3.0",
|
"@redwoodjs/api-server": "8.4.0",
|
||||||
"@redwoodjs/auth-dbauth-api": "8.3.0",
|
"@redwoodjs/auth-dbauth-api": "8.4.0",
|
||||||
"@redwoodjs/graphql-server": "8.3.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",
|
||||||
"graphql-scalars": "^1.23.0",
|
"graphql-scalars": "^1.23.0",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const schema = gql`
|
|||||||
discord
|
discord
|
||||||
twitch
|
twitch
|
||||||
linkedin
|
linkedin
|
||||||
|
matrix
|
||||||
github
|
github
|
||||||
gitea
|
gitea
|
||||||
forgejo
|
forgejo
|
||||||
|
|||||||
+3
-3
@@ -7,9 +7,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redwoodjs/auth-dbauth-setup": "8.3.0",
|
"@redwoodjs/auth-dbauth-setup": "8.4.0",
|
||||||
"@redwoodjs/core": "8.3.0",
|
"@redwoodjs/core": "8.4.0",
|
||||||
"@redwoodjs/project-config": "8.3.0",
|
"@redwoodjs/project-config": "8.4.0",
|
||||||
"prettier-plugin-tailwindcss": "0.4.1"
|
"prettier-plugin-tailwindcss": "0.4.1"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SiTiktokHex,
|
SiTiktokHex,
|
||||||
SiYoutubeHex,
|
SiYoutubeHex,
|
||||||
SiLinkedinHex,
|
SiLinkedinHex,
|
||||||
|
SiMatrixHex,
|
||||||
SiGithubHex,
|
SiGithubHex,
|
||||||
SiGiteaHex,
|
SiGiteaHex,
|
||||||
SiLeetcodeHex,
|
SiLeetcodeHex,
|
||||||
@@ -112,6 +113,10 @@ export const theme = {
|
|||||||
light: SiLinkedinHex,
|
light: SiLinkedinHex,
|
||||||
dark: SiLinkedinHex,
|
dark: SiLinkedinHex,
|
||||||
},
|
},
|
||||||
|
matrix: {
|
||||||
|
light: SiMatrixHex,
|
||||||
|
dark: invertColor(SiMatrixHex),
|
||||||
|
},
|
||||||
github: {
|
github: {
|
||||||
light: SiGithubHex,
|
light: SiGithubHex,
|
||||||
dark: invertColor(SiGithubHex),
|
dark: invertColor(SiGithubHex),
|
||||||
|
|||||||
+6
-6
@@ -14,11 +14,11 @@
|
|||||||
"@icons-pack/react-simple-icons": "^10.0.0",
|
"@icons-pack/react-simple-icons": "^10.0.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@redwoodjs/auth-dbauth-web": "8.3.0",
|
"@redwoodjs/auth-dbauth-web": "8.4.0",
|
||||||
"@redwoodjs/forms": "8.3.0",
|
"@redwoodjs/forms": "8.4.0",
|
||||||
"@redwoodjs/router": "8.3.0",
|
"@redwoodjs/router": "8.4.0",
|
||||||
"@redwoodjs/web": "8.3.0",
|
"@redwoodjs/web": "8.4.0",
|
||||||
"@redwoodjs/web-server": "8.3.0",
|
"@redwoodjs/web-server": "8.4.0",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tiptap/extension-link": "^2.8.0",
|
"@tiptap/extension-link": "^2.8.0",
|
||||||
"@tiptap/extension-text-style": "^2.8.0",
|
"@tiptap/extension-text-style": "^2.8.0",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"react-typed": "^2.0.12"
|
"react-typed": "^2.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redwoodjs/vite": "8.3.0",
|
"@redwoodjs/vite": "8.4.0",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/react-html-parser": "^2",
|
"@types/react-html-parser": "^2",
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
|||||||
className="btn btn-square btn-sm "
|
className="btn btn-square btn-sm "
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
toast.error(`Failed to paste, please try again`)
|
toast.error(`Failed to paste, please try again`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useState, useRef, useLayoutEffect } from 'react'
|
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 {
|
interface ContactCardProps {
|
||||||
portraitUrl: string
|
portraitUrl: string
|
||||||
|
socials: ContactCardPortrait['socials']
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactCard = ({ portraitUrl }: ContactCardProps) => {
|
const ContactCard = ({ portraitUrl, socials }: ContactCardProps) => {
|
||||||
const [width, setWidth] = useState()
|
const [width, setWidth] = useState()
|
||||||
const [height, setHeight] = useState()
|
const [height, setHeight] = useState()
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ const ContactCard = ({ portraitUrl }: ContactCardProps) => {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="p-2"></p>
|
<p className="p-2"></p>
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<SocialLinksCell />
|
<SocialLinks socials={socials} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { FindPortrait, FindPortraitVariables } from 'types/graphql'
|
import type {
|
||||||
|
ContactCardPortrait,
|
||||||
|
ContactCardPortraitVariables,
|
||||||
|
} from 'types/graphql'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TypedDocumentNode,
|
TypedDocumentNode,
|
||||||
@@ -11,25 +14,34 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
|
|||||||
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
|
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
|
||||||
import ContactCard from 'src/components/ContactCard/ContactCard/ContactCard'
|
import ContactCard from 'src/components/ContactCard/ContactCard/ContactCard'
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<FindPortrait, FindPortraitVariables> =
|
export const QUERY: TypedDocumentNode<
|
||||||
gql`
|
ContactCardPortrait,
|
||||||
query ContactCardPortrait {
|
ContactCardPortraitVariables
|
||||||
portrait: portrait {
|
> = gql`
|
||||||
fileId
|
query ContactCardPortrait {
|
||||||
}
|
portrait {
|
||||||
|
fileId
|
||||||
}
|
}
|
||||||
`
|
socials {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const Loading = () => <CellLoading />
|
export const Loading = () => <CellLoading />
|
||||||
|
|
||||||
export const Empty = () => <CellEmpty />
|
export const Empty = () => <CellEmpty />
|
||||||
|
|
||||||
export const Failure = ({ error }: CellFailureProps<FindPortrait>) => (
|
export const Failure = ({ error }: CellFailureProps<ContactCardPortrait>) => (
|
||||||
<CellFailure error={error} />
|
<CellFailure error={error} />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Success = ({
|
export const Success = ({
|
||||||
portrait,
|
portrait,
|
||||||
}: CellSuccessProps<FindPortrait, FindPortraitVariables>) => (
|
socials,
|
||||||
<ContactCard portraitUrl={portrait.fileId} />
|
}: CellSuccessProps<ContactCardPortrait, ContactCardPortraitVariables>) => (
|
||||||
|
<ContactCard portraitUrl={portrait.fileId} socials={socials} />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const types: FormSocial['type'][] = [
|
|||||||
'discord',
|
'discord',
|
||||||
'twitch',
|
'twitch',
|
||||||
'linkedin',
|
'linkedin',
|
||||||
|
'matrix',
|
||||||
'github',
|
'github',
|
||||||
'gitea',
|
'gitea',
|
||||||
'forgejo',
|
'forgejo',
|
||||||
@@ -52,6 +53,15 @@ const types: FormSocial['type'][] = [
|
|||||||
const urlHandles: FormSocial['type'][] = ['custom', 'gitea', 'forgejo']
|
const urlHandles: FormSocial['type'][] = ['custom', 'gitea', 'forgejo']
|
||||||
|
|
||||||
const SocialForm = (props: SocialFormProps) => {
|
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']>(
|
const [type, setType] = useState<FormSocial['type']>(
|
||||||
props.social?.type ?? 'x'
|
props.social?.type ?? 'x'
|
||||||
)
|
)
|
||||||
@@ -177,17 +187,20 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
pattern: {
|
pattern: {
|
||||||
value:
|
value:
|
||||||
type == 'email'
|
type == 'email'
|
||||||
? /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
? emailRegex
|
||||||
: type == 'phone'
|
: type == 'phone'
|
||||||
? /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
|
? phoneRegex
|
||||||
: urlHandles.includes(type) &&
|
: 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,
|
? urlRegex
|
||||||
|
: type == 'matrix' && matrixRegex,
|
||||||
message: `Invalid ${
|
message: `Invalid ${
|
||||||
urlHandles.includes(type)
|
urlHandles.includes(type)
|
||||||
? 'URL'
|
? 'URL'
|
||||||
: type == 'phone'
|
: type == 'phone'
|
||||||
? 'Phone Number'
|
? 'phone number'
|
||||||
: type == 'email' && 'Email'
|
: type == 'email'
|
||||||
|
? 'Email'
|
||||||
|
: type == 'matrix' && 'Matrix identifier'
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FindSocials } from 'types/graphql'
|
import { ContactCardPortrait } from 'types/graphql'
|
||||||
|
|
||||||
import { baseUrls, getLogoComponent, sortOrder } from 'src/lib/handle'
|
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`}>
|
<div className={`grid max-w-68 grid-cols-[repeat(auto-fit,3rem)] gap-2`}>
|
||||||
{[...socials]
|
{[...socials]
|
||||||
.sort((a, b) => sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type))
|
.sort((a, b) => sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type))
|
||||||
|
|||||||
@@ -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} />
|
|
||||||
)
|
|
||||||
@@ -8,7 +8,10 @@ const DARK_THEME = 'dark'
|
|||||||
|
|
||||||
const ThemeToggle = () => {
|
const ThemeToggle = () => {
|
||||||
const [theme, setTheme] = useState(
|
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>) => {
|
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SiTiktok,
|
SiTiktok,
|
||||||
SiYoutube,
|
SiYoutube,
|
||||||
SiLinkedin,
|
SiLinkedin,
|
||||||
|
SiMatrix,
|
||||||
SiGithub,
|
SiGithub,
|
||||||
SiGitea,
|
SiGitea,
|
||||||
SiLeetcode,
|
SiLeetcode,
|
||||||
@@ -33,6 +34,7 @@ export const baseUrls: Record<Handle, string> = {
|
|||||||
discord: 'https://discord.gg/',
|
discord: 'https://discord.gg/',
|
||||||
twitch: 'https://www.twitch.tv/',
|
twitch: 'https://www.twitch.tv/',
|
||||||
linkedin: 'https://www.linkedin.com/in/',
|
linkedin: 'https://www.linkedin.com/in/',
|
||||||
|
matrix: 'https://matrix.to/#/',
|
||||||
github: 'https://github.com/',
|
github: 'https://github.com/',
|
||||||
gitea: '',
|
gitea: '',
|
||||||
forgejo: '',
|
forgejo: '',
|
||||||
@@ -61,6 +63,7 @@ const logoComponents: Record<Handle, ReactElement> = {
|
|||||||
linkedin: (
|
linkedin: (
|
||||||
<SiLinkedin className="text-linkedin-light dark:text-linkedin-dark" />
|
<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" />,
|
github: <SiGithub className="text-github-light dark:text-github-dark" />,
|
||||||
gitea: <SiGitea className="text-gitea-light dark:text-gitea-dark" />,
|
gitea: <SiGitea className="text-gitea-light dark:text-gitea-dark" />,
|
||||||
forgejo: <SiForgejo className="text-forgejo-light dark:text-forgejo-dark" />,
|
forgejo: <SiForgejo className="text-forgejo-light dark:text-forgejo-dark" />,
|
||||||
@@ -80,6 +83,7 @@ export const sortOrder: Social['type'][] = [
|
|||||||
'phone',
|
'phone',
|
||||||
'email',
|
'email',
|
||||||
'custom',
|
'custom',
|
||||||
|
'matrix',
|
||||||
'linkedin',
|
'linkedin',
|
||||||
'leetcode',
|
'leetcode',
|
||||||
'github',
|
'github',
|
||||||
|
|||||||
Reference in New Issue
Block a user