Compare commits
10 Commits
e5f9bbd462
..
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6873c5c026 | |||
| 1b7e79c765 | |||
| 6e401cf2b3 | |||
| b89a5ee1b8 | |||
| 3c2b944bf4 | |||
| 11783069a8 | |||
| 835d895fc0 | |||
| 73ec75c167 | |||
| 49c943c9f3 | |||
| fb542bb5b5 |
+4
-4
@@ -15,8 +15,8 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
|
||||
# Ordered by how verbose they are: trace | debug | info | warn | error | silent
|
||||
# LOG_LEVEL=debug
|
||||
|
||||
FIRST_NAME=Ahmed
|
||||
LAST_NAME=Al-Taiar
|
||||
FIRST_NAME=firstname
|
||||
LAST_NAME=lastname
|
||||
|
||||
GMAIL=example@gmail.com
|
||||
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
||||
@@ -25,9 +25,9 @@ DOMAIN=example.com
|
||||
API_DOMAIN=api.example.com
|
||||
|
||||
# Must not end with "/"
|
||||
ADDRESS_PROD=https://example.com
|
||||
ADDRESS_PROD=https://portfolio.example.com
|
||||
ADDRESS_DEV=http://localhost:8910
|
||||
API_ADDRESS_PROD=https://api.example.com
|
||||
API_ADDRESS_PROD=https://api-portfolio.example.com
|
||||
API_ADDRESS_DEV=http://localhost:8911
|
||||
|
||||
MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||
|
||||
+2
-2
@@ -13,9 +13,9 @@ DOMAIN=example.com
|
||||
API_DOMAIN=api.example.com
|
||||
|
||||
# Must not end with "/"
|
||||
ADDRESS_PROD=https://example.com
|
||||
ADDRESS_PROD=https://portfolio.example.com
|
||||
ADDRESS_DEV=http://localhost:8910
|
||||
API_ADDRESS_PROD=https://api.example.com
|
||||
API_ADDRESS_PROD=https://api-portfolio.example.com
|
||||
API_ADDRESS_DEV=http://localhost:8911
|
||||
|
||||
MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
version: "1"
|
||||
name: Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Publish Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Registry
|
||||
run: echo "${{ secrets.ACCESS_TOKEN }}" | docker login git.altaiar.dev -u "${{ secrets.USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build & Tag Image
|
||||
run: |
|
||||
docker build -t git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} .
|
||||
docker tag git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} git.altaiar.dev/${{ gitea.repository }}:latest
|
||||
|
||||
- name: Push Images
|
||||
run: |
|
||||
docker push git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }}
|
||||
docker push git.altaiar.dev/${{ gitea.repository }}:latest
|
||||
+13
-5
@@ -37,8 +37,16 @@ FROM base as api_build
|
||||
|
||||
# If your api side build relies on build-time environment variables,
|
||||
# specify them here as ARGs. (But don't put secrets in your Dockerfile!)
|
||||
#
|
||||
# ARG MY_BUILD_TIME_ENV_VAR
|
||||
|
||||
ARG ADDRESS_PROD
|
||||
ARG ADDRESS_DEV
|
||||
ARG DOMAIN
|
||||
ARG API_DOMAIN
|
||||
ARG MAX_HTTP_CONNECTIONS_PER_MINUTE
|
||||
ARG GMAIL
|
||||
ARG GMAIL_SMTP_PASSWORD
|
||||
ARG FIRST_NAME
|
||||
ARG LAST_NAME
|
||||
|
||||
COPY --chown=node:node api api
|
||||
RUN yarn rw build api
|
||||
@@ -107,9 +115,9 @@ ENV NODE_ENV=production
|
||||
# If you are using a custom server file, you must use the following
|
||||
# command to launch your server instead of the default api-server below.
|
||||
# This is important if you intend to configure GraphQL to use Realtime.
|
||||
#
|
||||
# CMD [ "./api/dist/server.js" ]
|
||||
CMD [ "node_modules/.bin/rw-server", "api" ]
|
||||
|
||||
CMD [ "./api/dist/server.js" ]
|
||||
# CMD [ "node_modules/.bin/rw-server", "api" ]
|
||||
|
||||
# web serve
|
||||
# ---------
|
||||
|
||||
@@ -11,6 +11,22 @@ import { cookieName } from 'src/lib/auth'
|
||||
import { db } from 'src/lib/db'
|
||||
import { censorEmail, sendEmail } from 'src/lib/email'
|
||||
|
||||
function getCommonCookieDomain(domain: string, apiDomain: string): string {
|
||||
const splitDomain1 = domain.split('.').reverse()
|
||||
const splitDomain2 = apiDomain.split('.').reverse()
|
||||
const commonParts: string[] = []
|
||||
|
||||
for (let i = 0; i < Math.min(splitDomain1.length, splitDomain2.length); i++) {
|
||||
if (splitDomain1[i] === splitDomain2[i]) commonParts.push(splitDomain1[i])
|
||||
else break
|
||||
}
|
||||
|
||||
if (commonParts.length < 2)
|
||||
throw new Error('Domains do not share the same TLD')
|
||||
|
||||
return commonParts.reverse().join('.')
|
||||
}
|
||||
|
||||
export const handler = async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context: Context
|
||||
@@ -197,10 +213,8 @@ ${domain}/reset-password?resetToken=${resetToken}
|
||||
},
|
||||
|
||||
cors: {
|
||||
origin: isProduction
|
||||
? [process.env.ADDRESS_PROD, process.env.API_ADDRESS_PROD]
|
||||
: [process.env.ADDRESS_DEV, process.env.API_ADDRESS_DEV],
|
||||
credentials: true,
|
||||
origin: isProduction ? process.env.ADDRESS_PROD : process.env.ADDRESS_DEV,
|
||||
credentials: isProduction,
|
||||
methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
|
||||
},
|
||||
|
||||
@@ -218,8 +232,9 @@ ${domain}/reset-password?resetToken=${resetToken}
|
||||
Path: '/',
|
||||
SameSite: isProduction ? 'None' : 'Strict',
|
||||
Secure: isProduction,
|
||||
Domain: isProduction ? 'localhost' : 'localhost',
|
||||
// Domain: isProduction ? process.env.DOMAIN : 'localhost',
|
||||
Domain: isProduction
|
||||
? getCommonCookieDomain(process.env.DOMAIN, process.env.API_DOMAIN)
|
||||
: 'localhost',
|
||||
},
|
||||
name: cookieName,
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
HexColorCodeResolver,
|
||||
} from 'graphql-scalars'
|
||||
|
||||
import { isProduction } from '@redwoodjs/api/logger'
|
||||
import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
|
||||
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
||||
|
||||
@@ -32,5 +33,9 @@ export const handler = createGraphQLHandler({
|
||||
HexColorCode: HexColorCodeResolver,
|
||||
},
|
||||
},
|
||||
cors: {
|
||||
origin: isProduction ? process.env.ADDRESS_PROD : process.env.ADDRESS_DEV,
|
||||
credentials: isProduction,
|
||||
},
|
||||
onException: () => db.$disconnect(),
|
||||
})
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ import { handleTusUpload } from 'src/lib/tus'
|
||||
configureApiServer: async (server) => {
|
||||
await server.register(Cors, {
|
||||
origin: isProduction
|
||||
? [process.env.ADDRESS_PROD, process.env.API_ADDRESS_PROD]
|
||||
: [process.env.ADDRESS_DEV, process.env.API_ADDRESS_DEV],
|
||||
? process.env.ADDRESS_PROD
|
||||
: process.env.ADDRESS_DEV,
|
||||
methods: ['GET', 'POST', 'OPTIONS', 'PATCH', 'HEAD'],
|
||||
credentials: isProduction ? true : false,
|
||||
})
|
||||
|
||||
@@ -142,4 +142,16 @@ export const theme = {
|
||||
|
||||
export const darkMode = ['class', '[data-theme="dark"]']
|
||||
export const plugins = [require('daisyui')]
|
||||
export const daisyui = { themes: ['light', 'dark'] }
|
||||
export const daisyui = {
|
||||
themes: [
|
||||
'light',
|
||||
{
|
||||
dark: {
|
||||
...require('daisyui/src/theming/themes')['dark'],
|
||||
'base-100': '#212121',
|
||||
'base-200': '#1d1d1d',
|
||||
'base-300': '#191919',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,11 +27,11 @@
|
||||
"@uppy/progress-bar": "^4.0.0",
|
||||
"@uppy/react": "^4.0.1",
|
||||
"@uppy/tus": "^4.0.0",
|
||||
"@uppy/webcam": "^4.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"humanize-string": "2.1.0",
|
||||
"react": "18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "18.3.1",
|
||||
"react-typed": "^2.0.12"
|
||||
},
|
||||
|
||||
+8
-1
@@ -12,7 +12,14 @@ const App = () => (
|
||||
<FatalErrorBoundary page={FatalErrorPage}>
|
||||
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
||||
<AuthProvider>
|
||||
<RedwoodApolloProvider useAuth={useAuth}>
|
||||
<RedwoodApolloProvider
|
||||
useAuth={useAuth}
|
||||
graphQLClientConfig={{
|
||||
httpLinkConfig: {
|
||||
credentials: 'include',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
</AuthProvider>
|
||||
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
|
||||
|
||||
const dbAuthClient = createDbAuthClient()
|
||||
const dbAuthClient = createDbAuthClient({
|
||||
fetchConfig: {
|
||||
credentials: 'include',
|
||||
},
|
||||
})
|
||||
|
||||
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
|
||||
|
||||
@@ -22,6 +22,7 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
<HexColorInput color={color} className="w-16" />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-square btn-sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
@@ -35,6 +36,7 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
<Icon path={mdiContentCopy} className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-square btn-sm "
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef, useLayoutEffect } from 'react'
|
||||
|
||||
import SocialLinksCell from 'src/components/Social/SocialLinksCell'
|
||||
|
||||
@@ -12,7 +12,7 @@ const ContactCard = ({ portraitUrl }: ContactCardProps) => {
|
||||
|
||||
const observedDiv = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!observedDiv.current) return
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from '@mdi/react'
|
||||
|
||||
interface FormTextListProps {
|
||||
name: string
|
||||
hint?: string
|
||||
itemPlaceholder: string
|
||||
icon?: string
|
||||
list: string[]
|
||||
@@ -13,6 +14,7 @@ interface FormTextListProps {
|
||||
|
||||
const FormTextList = ({
|
||||
name,
|
||||
hint,
|
||||
itemPlaceholder,
|
||||
icon,
|
||||
list,
|
||||
@@ -23,15 +25,20 @@ const FormTextList = ({
|
||||
<div className="flex flex-col space-y-2 bg-base-100 rounded-xl">
|
||||
<div className="flex space-x-2 justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="font-semibold">{name}</p>
|
||||
<p className="font-semibold text-center">{name}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{hint && (
|
||||
<p className="opacity-70 text-xs font-light text-center">{hint}</p>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-square btn-sm"
|
||||
type="button"
|
||||
onClick={() => setList([...list, ''])}
|
||||
>
|
||||
<Icon path={mdiPlus} className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-square btn-sm"
|
||||
type="button"
|
||||
onClick={() => setList([...list, ''])}
|
||||
>
|
||||
<Icon path={mdiPlus} className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{list.map((item, i) => (
|
||||
<label
|
||||
@@ -53,11 +60,11 @@ const FormTextList = ({
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-square btn-sm flex-none"
|
||||
className="btn btn-square btn-error btn-sm flex-none"
|
||||
type="button"
|
||||
onClick={() => setList(list.filter((_, j) => j !== i))}
|
||||
>
|
||||
<Icon path={mdiDelete} className="size-4 text-error" />
|
||||
<Icon path={mdiDelete} className="size-4" />
|
||||
</button>
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Meta, UploadResult } from '@uppy/core'
|
||||
import type {
|
||||
@@ -56,13 +56,7 @@ const CREATE_PORTRAIT_MUTATION: TypedDocumentNode<
|
||||
`
|
||||
|
||||
const PortraitForm = ({ portrait }: PortraitFormProps) => {
|
||||
const [fileId, _setFileId] = useState<string>(portrait?.fileId)
|
||||
const fileIdRef = useRef<string>(fileId)
|
||||
|
||||
const setFileId = (fileId: string) => {
|
||||
_setFileId(fileId)
|
||||
fileIdRef.current = fileId
|
||||
}
|
||||
const [fileId, setFileId] = useState<string>(portrait?.fileId)
|
||||
|
||||
const unloadAbortController = new AbortController()
|
||||
|
||||
@@ -96,7 +90,7 @@ const PortraitForm = ({ portrait }: PortraitFormProps) => {
|
||||
setFileId(result.successful[0]?.uploadURL)
|
||||
window.addEventListener(
|
||||
'beforeunload',
|
||||
(e) => handleBeforeUnload(e, [fileIdRef.current]),
|
||||
(e) => handleBeforeUnload(e, [fileId]),
|
||||
{
|
||||
once: true,
|
||||
signal: unloadAbortController.signal,
|
||||
@@ -139,7 +133,7 @@ const PortraitForm = ({ portrait }: PortraitFormProps) => {
|
||||
<Uploader
|
||||
onComplete={onUploadComplete}
|
||||
width="22rem"
|
||||
height="11.5rem"
|
||||
height="34.5rem"
|
||||
className="flex justify-center"
|
||||
/>
|
||||
<p className="text-center">
|
||||
|
||||
@@ -46,18 +46,23 @@ const Project = ({ project }: Props) => {
|
||||
<>
|
||||
<h2 className="font-bold text-3xl w-fit">Links</h2>
|
||||
<div className="flex flex-col gap-2 w-fit">
|
||||
{project.links.map((link, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-wide"
|
||||
>
|
||||
<Icon path={mdiLinkVariant} className="size-5" />
|
||||
{link}
|
||||
</a>
|
||||
))}
|
||||
<ul className="list-none">
|
||||
{project.links.map((link, i) => (
|
||||
<li key={i}>
|
||||
<div className="flex gap-2 items-center justify-start">
|
||||
<Icon path={mdiLinkVariant} className="size-4" />
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item link link-hover"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -198,6 +198,7 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
<div className={`${!pickerVisible && 'pt-2'}`}>
|
||||
<FormTextList
|
||||
name="Links"
|
||||
hint="Short links are recommended"
|
||||
itemPlaceholder="URL"
|
||||
icon={mdiLinkVariant}
|
||||
list={links}
|
||||
@@ -246,16 +247,13 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
<div className="card-actions rounded-md justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-square btn-sm shadow-xl"
|
||||
className="btn btn-square btn-sm btn-error shadow-xl"
|
||||
onClick={() => {
|
||||
setToDelete([...toDelete, fileId])
|
||||
setFileIds(fileIds.filter((id) => id !== fileId))
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={mdiDelete}
|
||||
className="size-4 text-error"
|
||||
/>
|
||||
<Icon path={mdiDelete} className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Meta, UploadResult } from '@uppy/core'
|
||||
import type {
|
||||
@@ -55,13 +55,7 @@ const CREATE_RESUME_MUTATION: TypedDocumentNode<
|
||||
`
|
||||
|
||||
const ResumeForm = ({ resume }: ResumeFormProps) => {
|
||||
const [fileId, _setFileId] = useState<string>(resume?.fileId)
|
||||
const fileIdRef = useRef<string>(fileId)
|
||||
|
||||
const setFileId = (fileId: string) => {
|
||||
_setFileId(fileId)
|
||||
fileIdRef.current = fileId
|
||||
}
|
||||
const [fileId, setFileId] = useState<string>(resume?.fileId)
|
||||
|
||||
const unloadAbortController = new AbortController()
|
||||
|
||||
@@ -95,7 +89,7 @@ const ResumeForm = ({ resume }: ResumeFormProps) => {
|
||||
setFileId(result.successful[0]?.uploadURL)
|
||||
window.addEventListener(
|
||||
'beforeunload',
|
||||
(e) => handleBeforeUnload(e, [fileIdRef.current]),
|
||||
(e) => handleBeforeUnload(e, [fileId]),
|
||||
{
|
||||
once: true,
|
||||
signal: unloadAbortController.signal,
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { EditSocialById, UpdateSocialInput } from 'types/graphql'
|
||||
import type { RWGqlError } from '@redwoodjs/forms'
|
||||
import { Form, FieldError, Label, TextField, Submit } from '@redwoodjs/forms'
|
||||
|
||||
import { baseUrls, getLogoComponent } from 'src/lib/handle'
|
||||
import { baseUrls, getLogoComponent, sortOrder } from 'src/lib/handle'
|
||||
|
||||
type FormSocial = NonNullable<EditSocialById['social']>
|
||||
|
||||
@@ -233,19 +233,21 @@ const SocialForm = (props: SocialFormProps) => {
|
||||
tabIndex={0}
|
||||
className="menu dropdown-content z-10 mt-2 grid w-72 grid-cols-5 grid-rows-2 gap-2 rounded-box bg-base-100 shadow-xl"
|
||||
>
|
||||
{types.map((type, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
className="btn btn-square btn-ghost"
|
||||
onClick={() => {
|
||||
setType(type)
|
||||
setTypesDropdownOpen(false)
|
||||
}}
|
||||
>
|
||||
{getLogoComponent(type)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{types
|
||||
.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b))
|
||||
.map((type, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
className="btn btn-square btn-ghost"
|
||||
onClick={() => {
|
||||
setType(type)
|
||||
setTypesDropdownOpen(false)
|
||||
}}
|
||||
>
|
||||
{getLogoComponent(type)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { FindSocials } from 'types/graphql'
|
||||
|
||||
import { baseUrls, getLogoComponent } from 'src/lib/handle'
|
||||
import { baseUrls, getLogoComponent, sortOrder } from 'src/lib/handle'
|
||||
|
||||
const SocialLinks = ({ socials }: FindSocials) => {
|
||||
return (
|
||||
<div className={`grid max-w-68 grid-cols-[repeat(auto-fit,3rem)] gap-2`}>
|
||||
{[...socials]
|
||||
.sort((a, b) => (a.type > b.type ? 1 : -1))
|
||||
.map((social, i) => (
|
||||
<div key={i} className="tooltip" data-tip={social.name}>
|
||||
<a
|
||||
className="btn btn-square"
|
||||
href={`${baseUrls[social.type]}${social.username}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{getLogoComponent(social.type)}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SocialLinks = ({ socials }: FindSocials) => (
|
||||
<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))
|
||||
.map((social, i) => (
|
||||
<div key={i} className="tooltip" data-tip={social.name}>
|
||||
<a
|
||||
className="btn btn-square"
|
||||
href={`${baseUrls[social.type]}${social.username}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{getLogoComponent(social.type)}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default SocialLinks
|
||||
|
||||
@@ -13,7 +13,7 @@ import { toast } from '@redwoodjs/web/toast'
|
||||
|
||||
import { QUERY } from 'src/components/Social/SocialsCell'
|
||||
import { truncate } from 'src/lib/formatters'
|
||||
import { getLogoComponent } from 'src/lib/handle'
|
||||
import { getLogoComponent, sortOrder } from 'src/lib/handle'
|
||||
|
||||
const DELETE_SOCIAL_MUTATION: TypedDocumentNode<
|
||||
DeleteSocialMutation,
|
||||
@@ -58,66 +58,70 @@ const SocialsList = ({ socials }: FindSocials) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{socials.map((social) => {
|
||||
const actionButtons = (
|
||||
<>
|
||||
<Link
|
||||
to={routes.social({ id: social.id })}
|
||||
title={'Show social ' + social.id + ' detail'}
|
||||
className="btn btn-xs uppercase"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editSocial({ id: social.id })}
|
||||
title={'Edit social ' + social.id}
|
||||
className="btn btn-primary btn-xs uppercase"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete social ' + social.id}
|
||||
className="btn btn-error btn-xs uppercase"
|
||||
onClick={() => onDeleteClick(social.name, social.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
{[...socials]
|
||||
.sort(
|
||||
(a, b) => sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type)
|
||||
)
|
||||
.map((social) => {
|
||||
const actionButtons = (
|
||||
<>
|
||||
<Link
|
||||
to={routes.social({ id: social.id })}
|
||||
title={'Show social ' + social.id + ' detail'}
|
||||
className="btn btn-xs uppercase"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editSocial({ id: social.id })}
|
||||
title={'Edit social ' + social.id}
|
||||
className="btn btn-primary btn-xs uppercase"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete social ' + social.id}
|
||||
className="btn btn-error btn-xs uppercase"
|
||||
onClick={() => onDeleteClick(social.name, social.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<tr key={social.id}>
|
||||
<th>{getLogoComponent(social.type)}</th>
|
||||
<td>{truncate(social.name)}</td>
|
||||
<td>{truncate(social.username)}</td>
|
||||
<td>
|
||||
<nav className="hidden justify-end space-x-2 sm:flex">
|
||||
{actionButtons}
|
||||
</nav>
|
||||
<div className="dropdown dropdown-end flex justify-end sm:hidden">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-square btn-ghost btn-sm lg:hidden"
|
||||
>
|
||||
<Icon
|
||||
path={mdiDotsVertical}
|
||||
className="text-base-content-100 size-6"
|
||||
/>
|
||||
return (
|
||||
<tr key={social.id}>
|
||||
<th>{getLogoComponent(social.type)}</th>
|
||||
<td>{truncate(social.name)}</td>
|
||||
<td>{truncate(social.username)}</td>
|
||||
<td>
|
||||
<nav className="hidden justify-end space-x-2 sm:flex">
|
||||
{actionButtons}
|
||||
</nav>
|
||||
<div className="dropdown dropdown-end flex justify-end sm:hidden">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-square btn-ghost btn-sm lg:hidden"
|
||||
>
|
||||
<Icon
|
||||
path={mdiDotsVertical}
|
||||
className="text-base-content-100 size-6"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
className="menu dropdown-content z-10 -mt-1 mr-10 inline rounded-box bg-base-100 shadow-xl"
|
||||
>
|
||||
<nav className="w-46 space-x-2">{actionButtons}</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
className="menu dropdown-content z-10 -mt-1 mr-10 inline rounded-box bg-base-100 shadow-xl"
|
||||
>
|
||||
<nav className="w-46 space-x-2">{actionButtons}</nav>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ const DARK_THEME = 'dark'
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [theme, setTheme] = useState(
|
||||
localStorage.getItem('theme') ? localStorage.getItem('theme') : LIGHT_THEME
|
||||
localStorage.getItem('theme') ?? LIGHT_THEME
|
||||
)
|
||||
|
||||
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -37,13 +37,8 @@ const ThemeToggle = () => {
|
||||
checked={theme === DARK_THEME}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
path={mdiWeatherSunny}
|
||||
className="swap-off size-8 text-yellow-500"
|
||||
/>
|
||||
|
||||
<Icon path={mdiWeatherNight} className="swap-on size-8 text-blue-500" />
|
||||
<Icon path={mdiWeatherSunny} className="swap-off size-8 text-warning" />
|
||||
<Icon path={mdiWeatherNight} className="swap-on size-8 text-primary" />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ export const Titles = ({ titles, className }: TitlesProps) => {
|
||||
backDelay={1000}
|
||||
startWhenVisible
|
||||
loop
|
||||
onStringTyped={(pos, self) => {
|
||||
if (pos === 0) {
|
||||
self.stop()
|
||||
setTimeout(() => self.start(), 2500)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -56,6 +56,9 @@ const TitlesForm = ({ titles }: TitlesFormProps) => {
|
||||
|
||||
return (
|
||||
<Form onSubmit={onSubmit} className="max-w-80 space-y-2">
|
||||
<p className="text-center opacity-70">
|
||||
The first one gets displayed for longer
|
||||
</p>
|
||||
{Array.from({ length: MAX_TITLES }).map((_, i) => (
|
||||
<Label key={i} name={`title${i}`} className="form-control w-full">
|
||||
<Label
|
||||
|
||||
@@ -5,11 +5,13 @@ import Uppy from '@uppy/core'
|
||||
import type { UploadResult, Meta } from '@uppy/core'
|
||||
import { Dashboard } from '@uppy/react'
|
||||
import Tus from '@uppy/tus'
|
||||
import Webcam from '@uppy/webcam'
|
||||
|
||||
import { isProduction } from '@redwoodjs/api/logger'
|
||||
|
||||
import '@uppy/core/dist/style.min.css'
|
||||
import '@uppy/dashboard/dist/style.min.css'
|
||||
import '@uppy/webcam/dist/style.min.css'
|
||||
|
||||
type FileType = 'image' | 'pdf'
|
||||
|
||||
@@ -69,6 +71,11 @@ const Uploader = ({
|
||||
mimeType: 'image/webp',
|
||||
})
|
||||
|
||||
if (type === 'image')
|
||||
instance.use(Webcam, {
|
||||
modes: ['picture'],
|
||||
})
|
||||
|
||||
return instance.on('complete', onComplete)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const AccountbarLayout = ({ title, children }: AccountbarLayoutProps) => {
|
||||
<>
|
||||
<ToasterWrapper />
|
||||
<div className="sticky top-0 z-50 p-2">
|
||||
<div className="navbar rounded-xl bg-base-300 shadow-xl">
|
||||
<div className="navbar rounded-xl bg-base-300 backdrop-blur bg-opacity-90 shadow-xl">
|
||||
<div className="navbar-start">
|
||||
<p className="btn btn-ghost font-syne text-xl sm:hidden">{title}</p>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
||||
<>
|
||||
<ToasterWrapper />
|
||||
<div className="sticky top-0 z-50 p-2">
|
||||
<div className="navbar rounded-xl bg-base-300 shadow-xl">
|
||||
<div className="navbar rounded-xl bg-base-300 backdrop-blur bg-opacity-90 shadow-xl">
|
||||
<div className="navbar-start space-x-2 lg:first:space-x-0">
|
||||
<div className="dropdown">
|
||||
<div
|
||||
|
||||
@@ -25,7 +25,7 @@ const ScaffoldLayout = ({
|
||||
<>
|
||||
<ToasterWrapper />
|
||||
<div className="sticky top-0 z-50 p-2">
|
||||
<div className="navbar rounded-xl bg-base-300 font-syne shadow-xl">
|
||||
<div className="navbar rounded-xl bg-base-300 font-syne backdrop-blur bg-opacity-90 shadow-xl">
|
||||
<div className="navbar-start space-x-2">
|
||||
<Link to={routes.home()} className="btn btn-square btn-ghost">
|
||||
<Icon className="size-8" path={mdiHome} />
|
||||
|
||||
+23
-1
@@ -20,7 +20,7 @@ import {
|
||||
} from '@icons-pack/react-simple-icons'
|
||||
import { mdiEmail, mdiLink, mdiPhone } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import type { Handle } from 'types/graphql'
|
||||
import type { Handle, Social } from 'types/graphql'
|
||||
|
||||
export const baseUrls: Record<Handle, string> = {
|
||||
x: 'https://x.com/',
|
||||
@@ -76,4 +76,26 @@ const logoComponents: Record<Handle, ReactElement> = {
|
||||
custom: <Icon path={mdiLink} className="size-7" />,
|
||||
}
|
||||
|
||||
export const sortOrder: Social['type'][] = [
|
||||
'phone',
|
||||
'email',
|
||||
'custom',
|
||||
'linkedin',
|
||||
'leetcode',
|
||||
'github',
|
||||
'gitea',
|
||||
'forgejo',
|
||||
'gitlab',
|
||||
'bitbucket',
|
||||
'youtube',
|
||||
'x',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'facebook',
|
||||
'threads',
|
||||
'twitch',
|
||||
'discord',
|
||||
'steam',
|
||||
]
|
||||
|
||||
export const getLogoComponent = (type: Handle) => logoComponents[type]
|
||||
|
||||
+1
-2
@@ -8,9 +8,8 @@ export const deleteFile = async (fileId: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const handleBeforeUnload = (_e: BeforeUnloadEvent, files: string[]) => {
|
||||
export const handleBeforeUnload = (_e: BeforeUnloadEvent, files: string[]) =>
|
||||
batchDelete(files)
|
||||
}
|
||||
|
||||
export const batchDelete = (files: string[]) => {
|
||||
for (const file of files) deleteFile(file)
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
import { mdiCompass, mdiContacts } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
import TitlesCell from 'src/components/Title/TitlesCell'
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<Metadata title="Home" />
|
||||
const HomePage = () => (
|
||||
<>
|
||||
<Metadata title="Home" />
|
||||
|
||||
<div className="hero min-h-64">
|
||||
<div className="hero-content">
|
||||
<div className="max-w-xl text-center">
|
||||
<TitlesCell className="text-primary" />
|
||||
</div>
|
||||
<div className="hero min-h-[calc(100vh-6rem)]">
|
||||
<div className="hero-content flex flex-col gap-8">
|
||||
<div className="text-center">
|
||||
<TitlesCell className="text-primary" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
to={routes.projects()}
|
||||
className="btn btn-primary btn-outline sm:btn-wide btn-lg"
|
||||
>
|
||||
<Icon path={mdiCompass} className="size-6" />
|
||||
Explore
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.contact()}
|
||||
className="btn btn-primary btn-outline sm:btn-wide btn-lg"
|
||||
>
|
||||
<Icon path={mdiContacts} className="size-6" />
|
||||
Contact
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export default HomePage
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isMobile, isBrowser } from 'react-device-detect'
|
||||
import mobile from 'is-mobile'
|
||||
|
||||
import { Metadata } from '@redwoodjs/web'
|
||||
|
||||
@@ -14,8 +14,12 @@ const ProjectsPage = () => {
|
||||
<div className="max-w-md text-center">
|
||||
<h1 className="text-5xl font-bold">Projects</h1>
|
||||
<p className="py-6">
|
||||
{isBrowser && !isMobile ? 'Click' : 'Tap'} on a project for
|
||||
details
|
||||
{mobile({
|
||||
tablet: true,
|
||||
})
|
||||
? 'Tap'
|
||||
: 'Click'}{' '}
|
||||
on a project for details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6278,6 +6278,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/webcam@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "@uppy/webcam@npm:4.0.1"
|
||||
dependencies:
|
||||
"@uppy/utils": "npm:^6.0.1"
|
||||
is-mobile: "npm:^4.0.0"
|
||||
preact: "npm:^10.5.13"
|
||||
peerDependencies:
|
||||
"@uppy/core": ^4.1.0
|
||||
checksum: 10c0/b7522eb797e16d02ff3b2e106c713d7e7cccc6a0b200db0464b56dc2f4d22ed173f7dd2d1c6a64641f6a84784eb7310d72d1de4ffcdd0391648444b5820dc17a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@vitejs/plugin-react@npm:4.3.1"
|
||||
@@ -11773,6 +11786,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-mobile@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "is-mobile@npm:4.0.0"
|
||||
checksum: 10c0/7d1f1c9ead3f140728318df7b1d6f2f19f28d96bf09c3a9016fe473ccccd32c4d03a01aeec68b612d48f1c0f776e7f1f18a1d83a7e95fb8199b4eb8536db01bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-nan@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "is-nan@npm:1.3.2"
|
||||
@@ -15476,18 +15496,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-device-detect@npm:^2.2.3":
|
||||
version: 2.2.3
|
||||
resolution: "react-device-detect@npm:2.2.3"
|
||||
dependencies:
|
||||
ua-parser-js: "npm:^1.0.33"
|
||||
peerDependencies:
|
||||
react: ">= 0.14.0"
|
||||
react-dom: ">= 0.14.0"
|
||||
checksum: 10c0/396bbeeab0cb21da084c67434d204c9cf502fad6c683903313084d3f6487950a36a34f9bf67ccf5c1772a1bb5b79a2a4403fcfe6b51d93877db4c2d9f3a3a925
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:18.3.1":
|
||||
version: 18.3.1
|
||||
resolution: "react-dom@npm:18.3.1"
|
||||
@@ -17697,15 +17705,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ua-parser-js@npm:^1.0.33":
|
||||
version: 1.0.39
|
||||
resolution: "ua-parser-js@npm:1.0.39"
|
||||
bin:
|
||||
ua-parser-js: script/cli.js
|
||||
checksum: 10c0/c6452b0c683000f10975cb0a7e74cb1119ea95d4522ae85f396fa53b0b17884358a24ffdd86a66030c6b2981bdc502109a618c79fdaa217ee9032c9e46fcc78a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ua-parser-js@npm:^1.0.35":
|
||||
version: 1.0.38
|
||||
resolution: "ua-parser-js@npm:1.0.38"
|
||||
@@ -18198,6 +18197,7 @@ __metadata:
|
||||
"@uppy/progress-bar": "npm:^4.0.0"
|
||||
"@uppy/react": "npm:^4.0.1"
|
||||
"@uppy/tus": "npm:^4.0.0"
|
||||
"@uppy/webcam": "npm:^4.0.1"
|
||||
autoprefixer: "npm:^10.4.20"
|
||||
daisyui: "npm:^4.12.10"
|
||||
date-fns: "npm:^4.1.0"
|
||||
@@ -18206,7 +18206,6 @@ __metadata:
|
||||
postcss-loader: "npm:^8.1.1"
|
||||
react: "npm:18.3.1"
|
||||
react-colorful: "npm:^5.6.1"
|
||||
react-device-detect: "npm:^2.2.3"
|
||||
react-dom: "npm:18.3.1"
|
||||
react-typed: "npm:^2.0.12"
|
||||
tailwindcss: "npm:^3.4.8"
|
||||
|
||||
Reference in New Issue
Block a user