Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
7973663b2a | |||
6540329f36 | |||
bac5b5fe48 | |||
f3f75d3e57 | |||
f14732cdf0 | |||
77db153fe6 | |||
684d6f88c2 | |||
03f606bbde | |||
1eafaee2c0 |
@ -18,8 +18,13 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
|
||||
FIRST_NAME=firstname
|
||||
LAST_NAME=lastname
|
||||
|
||||
GMAIL=example@gmail.com
|
||||
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=noreply@example.com
|
||||
EMAIL_FROM=noreply@example.com
|
||||
EMAIL_TO=email@example.com
|
||||
SMTP_PASSWORD=password
|
||||
|
||||
DOMAIN=example.com
|
||||
API_DOMAIN=api.example.com
|
||||
|
@ -6,8 +6,13 @@
|
||||
FIRST_NAME=firstname
|
||||
LAST_NAME=lastname
|
||||
|
||||
GMAIL=example@gmail.com
|
||||
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=noreply@example.com
|
||||
EMAIL_FROM=noreply@example.com
|
||||
EMAIL_TO=email@example.com
|
||||
SMTP_PASSWORD=password
|
||||
|
||||
DOMAIN=example.com
|
||||
API_DOMAIN=api.example.com
|
||||
|
@ -43,8 +43,13 @@ ARG ADDRESS_DEV
|
||||
ARG DOMAIN
|
||||
ARG API_DOMAIN
|
||||
ARG MAX_HTTP_CONNECTIONS_PER_MINUTE
|
||||
ARG GMAIL
|
||||
ARG GMAIL_SMTP_PASSWORD
|
||||
ARG SMTP_HOST
|
||||
ARG SMTP_PORT
|
||||
ARG SMTP_SECURE
|
||||
ARG SMTP_USER
|
||||
ARG SMTP_PASSWORD
|
||||
ARG EMAIL_FROM
|
||||
ARG EMAIL_TO
|
||||
ARG FIRST_NAME
|
||||
ARG LAST_NAME
|
||||
|
||||
|
39
README.md
39
README.md
@ -6,10 +6,8 @@ Create two A records, one for the web side of the website and one for the api si
|
||||
- It doesn't matter what reverse proxy you use (Nginx, Apache, Traefik, Caddy, etc)
|
||||
1. Point the web domain to the web port (default: 8910)
|
||||
2. Point the api domain to the api port (default: 8911)
|
||||
### Gmail App Password
|
||||
1. Go to your Google [account dashboard](https://myaccount.google.com)
|
||||
2. Go to Security > 2-Step Verification > App Passwords > Create a new app password
|
||||
3. Copy the 16 character password
|
||||
### SMTP
|
||||
You will need credentials to authorize sending Email, instructions vary depending on provider (Gmail, Hotmail, etc).
|
||||
### [Docker Compose](./docker-compose.yml)
|
||||
```yaml
|
||||
version: '3.8'
|
||||
@ -23,21 +21,28 @@ 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:redwood@db:5432/portfolio
|
||||
- DATABASE_URL=postgresql://redwood:changeme@db:5432/portfolio
|
||||
- FIRST_NAME=first name # Your first name
|
||||
- LAST_NAME=lastname # Your last name
|
||||
- GMAIL=example@gmail.com # The Gmail address associated with the app password created earlier
|
||||
- GMAIL_SMTP_PASSWORD=aaaa bbbb cccc dddd # The app password created earlier
|
||||
- SMTP_HOST=smtp.example.com
|
||||
- SMTP_PORT=465
|
||||
- SMTP_SECURE=true
|
||||
- SMTP_USER=noreply@example.com
|
||||
- EMAIL_FROM=noreply@example.com
|
||||
- EMAIL_TO=email@example.com
|
||||
- SMTP_PASSWORD=password
|
||||
- DOMAIN=portfolio.example.com
|
||||
- API_DOMAIN=api.portfolio.example.com
|
||||
# Careful, addresses below must not end with a '/'
|
||||
- ADDRESS_PROD=https://portfolio.example.com # https://DOMAIN
|
||||
- API_ADDRESS_PROD=https://api.portfolio.example.com # https://API_DOMAIN
|
||||
ports:
|
||||
- '8910:8910' # Web
|
||||
- '8911:8911' # API
|
||||
- 8910:8910 # Web
|
||||
- 8911:8911 # API
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- files:/home/node/app/api/files_prod
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
yarn rw build &&
|
||||
@ -50,16 +55,22 @@ services:
|
||||
container_name: portfolio-db
|
||||
image: postgres:16-bookworm
|
||||
environment:
|
||||
POSTGRES_USER: redwood
|
||||
POSTGRES_PASSWORD: redwood
|
||||
POSTGRES_DB: portfolio
|
||||
- POSTGRES_USER=redwood
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
- POSTGRES_DB=portfolio
|
||||
ports:
|
||||
- '5432:5432'
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
files: # For persistent file storage across upgrades
|
||||
```
|
||||
## Fix Files Ownership
|
||||
The `files` volume in Docker is owned by `root`, since the portfolio container runs under the `node` user, file uploads will fail. Run this command to give ownership to the `node` user:
|
||||
```
|
||||
sudo docker exec -u root portfolio chown -R node:node /home/node/app/api/files_prod
|
||||
```
|
||||
## Logging In
|
||||
- Once the container is up and running, head to `/login` (`https://portfolio.example.com/login`), default credentials are below
|
||||
@ -69,4 +80,4 @@ volumes:
|
||||
### Default Credentials
|
||||
**Username:** `admin`
|
||||
|
||||
**Password:** [`GMAIL_SMTP_PASSWORD`](#gmail-app-password)
|
||||
**Password:** [`SMTP_PASSWORD`](#smtp)
|
||||
|
2
api/db/migrations/20241015183037_matrix/migration.sql
Normal file
2
api/db/migrations/20241015183037_matrix/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "Handle" ADD VALUE 'matrix';
|
@ -25,6 +25,7 @@ enum Handle {
|
||||
discord
|
||||
twitch
|
||||
linkedin
|
||||
matrix
|
||||
github
|
||||
gitea
|
||||
forgejo
|
||||
|
@ -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",
|
||||
|
@ -17,6 +17,7 @@ export const schema = gql`
|
||||
discord
|
||||
twitch
|
||||
linkedin
|
||||
matrix
|
||||
github
|
||||
gitea
|
||||
forgejo
|
||||
|
@ -8,16 +8,18 @@ interface Options {
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.GMAIL,
|
||||
pass: process.env.GMAIL_SMTP_PASSWORD,
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
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}>`,
|
||||
from: `${process.env.FIRST_NAME} ${process.env.LAST_NAME} <${process.env.EMAIL_FROM}>`,
|
||||
to: Array.isArray(to) ? to : [to],
|
||||
subject,
|
||||
text,
|
||||
|
@ -9,21 +9,28 @@ 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:redwood@db:5432/portfolio
|
||||
- DATABASE_URL=postgresql://redwood:changeme@db:5432/portfolio
|
||||
- FIRST_NAME=first name # Your first name
|
||||
- LAST_NAME=lastname # Your last name
|
||||
- GMAIL=example@gmail.com # The Gmail address associated with the app password created earlier
|
||||
- GMAIL_SMTP_PASSWORD=aaaa bbbb cccc dddd # The app password created earlier
|
||||
- SMTP_HOST=smtp.example.com
|
||||
- SMTP_PORT=465
|
||||
- SMTP_SECURE=true
|
||||
- SMTP_USER=noreply@example.com
|
||||
- EMAIL_FROM=noreply@example.com
|
||||
- EMAIL_TO=email@example.com
|
||||
- SMTP_PASSWORD=password
|
||||
- DOMAIN=portfolio.example.com
|
||||
- API_DOMAIN=api.portfolio.example.com
|
||||
# Careful, addresses below must not end with a '/'
|
||||
- ADDRESS_PROD=https://portfolio.example.com # https://DOMAIN
|
||||
- API_ADDRESS_PROD=https://api.portfolio.example.com # https://API_DOMAIN
|
||||
ports:
|
||||
- '8910:8910' # Web
|
||||
- '8911:8911' # API
|
||||
- 8910:8910 # Web
|
||||
- 8911:8911 # API
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- files:/home/node/app/api/files_prod
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
yarn rw build &&
|
||||
@ -36,13 +43,14 @@ services:
|
||||
container_name: portfolio-db
|
||||
image: postgres:16-bookworm
|
||||
environment:
|
||||
POSTGRES_USER: redwood
|
||||
POSTGRES_PASSWORD: redwood
|
||||
POSTGRES_DB: portfolio
|
||||
- POSTGRES_USER=redwood
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
- POSTGRES_DB=portfolio
|
||||
ports:
|
||||
- '5432:5432'
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
files: # For persistent file storage across upgrades
|
||||
|
@ -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": {
|
||||
|
@ -9,15 +9,15 @@ export default async () => {
|
||||
try {
|
||||
const admin = {
|
||||
username: 'admin',
|
||||
email: process.env.GMAIL,
|
||||
password: process.env.GMAIL_SMTP_PASSWORD,
|
||||
email: process.env.EMAIL_TO,
|
||||
password: process.env.SMTP_PASSWORD,
|
||||
}
|
||||
|
||||
const [hashedPassword, salt] = hashPassword(admin.password)
|
||||
|
||||
const existingAdmin = await db.user.findFirst({
|
||||
where: {
|
||||
email: admin.email,
|
||||
username: admin.username,
|
||||
},
|
||||
})
|
||||
|
||||
@ -30,6 +30,14 @@ export default async () => {
|
||||
salt,
|
||||
},
|
||||
})
|
||||
else
|
||||
await db.user.update({
|
||||
where: { id: existingAdmin.id },
|
||||
data: {
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
},
|
||||
})
|
||||
|
||||
const titles = await db.titles.findFirst()
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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`)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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`
|
||||
query ContactCardPortrait {
|
||||
portrait: portrait {
|
||||
fileId
|
||||
}
|
||||
export const QUERY: TypedDocumentNode<
|
||||
ContactCardPortrait,
|
||||
ContactCardPortraitVariables
|
||||
> = gql`
|
||||
query ContactCardPortrait {
|
||||
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} />
|
||||
)
|
||||
|
@ -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'
|
||||
}`,
|
||||
},
|
||||
}}
|
||||
|
@ -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))
|
||||
|
@ -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,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',
|
||||
|
Reference in New Issue
Block a user