Compare commits
2 Commits
22b2e25875
...
430a2da835
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430a2da835 | ||
|
|
43be1abf96 |
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Project" ALTER COLUMN "date" DROP DEFAULT;
|
||||||
10
api/db/migrations/20240921181721_more_socials/migration.sql
Normal file
10
api/db/migrations/20240921181721_more_socials/migration.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
-- This migration adds more than one value to an enum.
|
||||||
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||||
|
-- in a single migration. This can be worked around by creating
|
||||||
|
-- multiple migrations, each migration adding only one value to
|
||||||
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'gitea';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'leetcode';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
-- This migration adds more than one value to an enum.
|
||||||
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||||
|
-- in a single migration. This can be worked around by creating
|
||||||
|
-- multiple migrations, each migration adding only one value to
|
||||||
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'steam';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'discord';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'twitch';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'forgejo';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'gitlab';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'bitbucket';
|
||||||
|
ALTER TYPE "Handle" ADD VALUE 'phone';
|
||||||
@@ -21,9 +21,18 @@ enum Handle {
|
|||||||
facebook
|
facebook
|
||||||
tiktok
|
tiktok
|
||||||
youtube
|
youtube
|
||||||
|
steam
|
||||||
|
discord
|
||||||
|
twitch
|
||||||
linkedin
|
linkedin
|
||||||
github
|
github
|
||||||
|
gitea
|
||||||
|
forgejo
|
||||||
|
gitlab
|
||||||
|
bitbucket
|
||||||
|
leetcode
|
||||||
email
|
email
|
||||||
|
phone
|
||||||
custom
|
custom
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +78,7 @@ model Project {
|
|||||||
title String
|
title String
|
||||||
description String @default("No description provided")
|
description String @default("No description provided")
|
||||||
images ProjectImage[]
|
images ProjectImage[]
|
||||||
date DateTime @default(now())
|
date DateTime
|
||||||
links String[] @default([])
|
links String[] @default([])
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,18 @@ export const schema = gql`
|
|||||||
facebook
|
facebook
|
||||||
tiktok
|
tiktok
|
||||||
youtube
|
youtube
|
||||||
|
steam
|
||||||
|
discord
|
||||||
|
twitch
|
||||||
linkedin
|
linkedin
|
||||||
github
|
github
|
||||||
|
gitea
|
||||||
|
forgejo
|
||||||
|
gitlab
|
||||||
|
bitbucket
|
||||||
|
leetcode
|
||||||
email
|
email
|
||||||
|
phone
|
||||||
custom
|
custom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
MutationResolvers,
|
MutationResolvers,
|
||||||
CreateSocialInput,
|
CreateSocialInput,
|
||||||
UpdateSocialInput,
|
UpdateSocialInput,
|
||||||
|
Handle,
|
||||||
} from 'types/graphql'
|
} from 'types/graphql'
|
||||||
|
|
||||||
import { ValidationError } from '@redwoodjs/graphql-server'
|
import { ValidationError } from '@redwoodjs/graphql-server'
|
||||||
@@ -12,6 +13,8 @@ import { db } from 'src/lib/db'
|
|||||||
const urlRegex =
|
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
|
/^(?:(?:(?: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 phoneRegex = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
|
||||||
|
|
||||||
const emailRegex =
|
const emailRegex =
|
||||||
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
||||||
|
|
||||||
@@ -50,8 +53,12 @@ const validateInput = (input: CreateSocialInput | UpdateSocialInput) => {
|
|||||||
throw new ValidationError('Name is required')
|
throw new ValidationError('Name is required')
|
||||||
if (!input.type) throw new ValidationError('Type is required')
|
if (!input.type) throw new ValidationError('Type is required')
|
||||||
|
|
||||||
if (input.type === 'custom' && !urlRegex.test(input.username))
|
const urlHandles: Handle[] = ['custom', 'gitea', 'forgejo']
|
||||||
|
|
||||||
|
if (urlHandles.includes(input.type) && !urlRegex.test(input.username))
|
||||||
throw new ValidationError('Invalid URL')
|
throw new ValidationError('Invalid URL')
|
||||||
|
else if (input.type === 'phone' && !phoneRegex.test(input.username))
|
||||||
|
throw new ValidationError('Invalid Phone Number')
|
||||||
else if (input.type === 'email' && !emailRegex.test(input.username))
|
else if (input.type === 'email' && !emailRegex.test(input.username))
|
||||||
throw new ValidationError('Invalid Email')
|
throw new ValidationError('Invalid Email')
|
||||||
else if (input.username.trim().length === 0)
|
else if (input.username.trim().length === 0)
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import {
|
|||||||
SiYoutubeHex,
|
SiYoutubeHex,
|
||||||
SiLinkedinHex,
|
SiLinkedinHex,
|
||||||
SiGithubHex,
|
SiGithubHex,
|
||||||
|
SiGiteaHex,
|
||||||
|
SiLeetcodeHex,
|
||||||
|
SiSteamHex,
|
||||||
|
SiDiscordHex,
|
||||||
|
SiTwitchHex,
|
||||||
|
SiForgejoHex,
|
||||||
|
SiGitlabHex,
|
||||||
|
SiBitbucketHex,
|
||||||
} from '@icons-pack/react-simple-icons'
|
} from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
const invertColor = (hex) => {
|
const invertColor = (hex) => {
|
||||||
@@ -36,6 +44,9 @@ export const theme = {
|
|||||||
maxWidth: {
|
maxWidth: {
|
||||||
68: '17rem',
|
68: '17rem',
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
128: '32rem',
|
||||||
|
},
|
||||||
aspectRatio: {
|
aspectRatio: {
|
||||||
portrait: '4 / 5',
|
portrait: '4 / 5',
|
||||||
},
|
},
|
||||||
@@ -84,6 +95,19 @@ export const theme = {
|
|||||||
light: SiYoutubeHex,
|
light: SiYoutubeHex,
|
||||||
dark: SiYoutubeHex,
|
dark: SiYoutubeHex,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
steam: {
|
||||||
|
light: SiSteamHex,
|
||||||
|
dark: invertColor(SiSteamHex),
|
||||||
|
},
|
||||||
|
discord: {
|
||||||
|
light: SiDiscordHex,
|
||||||
|
dark: SiDiscordHex,
|
||||||
|
},
|
||||||
|
twitch: {
|
||||||
|
light: SiTwitchHex,
|
||||||
|
dark: SiTwitchHex,
|
||||||
|
},
|
||||||
linkedin: {
|
linkedin: {
|
||||||
light: SiLinkedinHex,
|
light: SiLinkedinHex,
|
||||||
dark: SiLinkedinHex,
|
dark: SiLinkedinHex,
|
||||||
@@ -92,6 +116,26 @@ export const theme = {
|
|||||||
light: SiGithubHex,
|
light: SiGithubHex,
|
||||||
dark: invertColor(SiGithubHex),
|
dark: invertColor(SiGithubHex),
|
||||||
},
|
},
|
||||||
|
gitea: {
|
||||||
|
light: SiGiteaHex,
|
||||||
|
dark: SiGiteaHex,
|
||||||
|
},
|
||||||
|
forgejo: {
|
||||||
|
light: SiForgejoHex,
|
||||||
|
dark: SiForgejoHex,
|
||||||
|
},
|
||||||
|
gitlab: {
|
||||||
|
light: SiGitlabHex,
|
||||||
|
dark: SiGitlabHex,
|
||||||
|
},
|
||||||
|
bitbucket: {
|
||||||
|
light: SiBitbucketHex,
|
||||||
|
dark: SiBitbucketHex,
|
||||||
|
},
|
||||||
|
leetcode: {
|
||||||
|
light: SiLeetcodeHex,
|
||||||
|
dark: SiLeetcodeHex,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@uppy/progress-bar": "^4.0.0",
|
"@uppy/progress-bar": "^4.0.0",
|
||||||
"@uppy/react": "^4.0.1",
|
"@uppy/react": "^4.0.1",
|
||||||
"@uppy/tus": "^4.0.0",
|
"@uppy/tus": "^4.0.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"humanize-string": "2.1.0",
|
"humanize-string": "2.1.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
|
|||||||
103
web/src/components/DatePicker/DatePicker.tsx
Normal file
103
web/src/components/DatePicker/DatePicker.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||||
|
import Icon from '@mdi/react'
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
eachDayOfInterval,
|
||||||
|
endOfMonth,
|
||||||
|
endOfWeek,
|
||||||
|
format,
|
||||||
|
getDay,
|
||||||
|
isEqual,
|
||||||
|
isSameMonth,
|
||||||
|
isToday,
|
||||||
|
parse,
|
||||||
|
startOfWeek,
|
||||||
|
sub,
|
||||||
|
} from 'date-fns'
|
||||||
|
|
||||||
|
interface DatePickerProps {
|
||||||
|
date: Date
|
||||||
|
setDate: React.Dispatch<React.SetStateAction<Date>>
|
||||||
|
month: string
|
||||||
|
setMonth: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatePicker = ({ date, setDate, month, setMonth }: DatePickerProps) => {
|
||||||
|
const currentMonthFirstDay = parse(month, 'MMMM yyyy', new Date())
|
||||||
|
|
||||||
|
const days = eachDayOfInterval({
|
||||||
|
start: startOfWeek(currentMonthFirstDay),
|
||||||
|
end: endOfWeek(endOfMonth(currentMonthFirstDay)),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-fit bg-base-100 space-y-2 p-2 rounded-xl">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setMonth(
|
||||||
|
format(sub(currentMonthFirstDay, { months: 1 }), 'MMMM yyyy')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="btn btn-sm btn-square btn-ghost"
|
||||||
|
>
|
||||||
|
<Icon path={mdiChevronLeft} className="size-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p>{format(currentMonthFirstDay, 'MMMM yyyy')}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setMonth(
|
||||||
|
format(add(currentMonthFirstDay, { months: 1 }), 'MMMM yyyy')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="btn btn-sm btn-square btn-ghost"
|
||||||
|
>
|
||||||
|
<Icon path={mdiChevronRight} className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 border-y py-1 border-base-300 grid-cols-7 text-center">
|
||||||
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S '].map((weekday, i) => (
|
||||||
|
<div key={i}>{weekday}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 grid-cols-7 text-center">
|
||||||
|
{days.map((day, i) => {
|
||||||
|
const selected = isEqual(day, date)
|
||||||
|
const isCurrentMonth = isSameMonth(day, currentMonthFirstDay)
|
||||||
|
const today = isToday(day)
|
||||||
|
const weekday = getDay(day)
|
||||||
|
|
||||||
|
let btnColor = 'btn-ghost'
|
||||||
|
|
||||||
|
if (!isCurrentMonth) btnColor += ' opacity-40'
|
||||||
|
|
||||||
|
if (today) btnColor = 'btn-error'
|
||||||
|
if (selected) btnColor = 'btn-primary'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={day.toString()}
|
||||||
|
className={`${i === 0 && ['col-start-1', 'col-start-2', 'col-start-3', 'col-start-4', 'col-start-5', 'col-start-6', 'col-start-7'][weekday]}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDate(day)}
|
||||||
|
className={`btn btn-sm btn-square ${btnColor}`}
|
||||||
|
>
|
||||||
|
<time dateTime={format(day, 'yyyy-MM-dd')}>
|
||||||
|
{format(day, 'd')}
|
||||||
|
</time>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatePicker
|
||||||
@@ -74,9 +74,15 @@ const Project = ({ project }: Props) => {
|
|||||||
<th>Links</th>
|
<th>Links</th>
|
||||||
<td className="space-x-2 space-y-2">
|
<td className="space-x-2 space-y-2">
|
||||||
{project.links.map((link, i) => (
|
{project.links.map((link, i) => (
|
||||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
className="badge badge-ghost text-nowrap"
|
||||||
|
key={i}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
{link}
|
{link}
|
||||||
</div>
|
</a>
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { mdiFormatTitle, mdiLinkVariant } from '@mdi/js'
|
import { mdiCalendar, mdiFormatTitle, mdiLinkVariant } from '@mdi/js'
|
||||||
import Icon from '@mdi/react'
|
import Icon from '@mdi/react'
|
||||||
|
import { format, isAfter, startOfToday } from 'date-fns'
|
||||||
import type { EditProjectById, UpdateProjectInput } from 'types/graphql'
|
import type { EditProjectById, UpdateProjectInput } from 'types/graphql'
|
||||||
|
|
||||||
import type { RWGqlError } from '@redwoodjs/forms'
|
import type { RWGqlError } from '@redwoodjs/forms'
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
} from '@redwoodjs/forms'
|
} from '@redwoodjs/forms'
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
|
import DatePicker from 'src/components/DatePicker/DatePicker'
|
||||||
import FormTextList from 'src/components/FormTextList/FormTextList'
|
import FormTextList from 'src/components/FormTextList/FormTextList'
|
||||||
|
|
||||||
type FormProject = NonNullable<EditProjectById['project']>
|
type FormProject = NonNullable<EditProjectById['project']>
|
||||||
@@ -29,8 +31,15 @@ interface ProjectFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProjectForm = (props: ProjectFormProps) => {
|
const ProjectForm = (props: ProjectFormProps) => {
|
||||||
|
const today = startOfToday()
|
||||||
|
|
||||||
const [links, setLinks] = useState<string[]>(props.project?.links || [])
|
const [links, setLinks] = useState<string[]>(props.project?.links || [])
|
||||||
const [linkErrors, setLinkErrors] = useState<boolean[]>([])
|
const [linkErrors, setLinkErrors] = useState<boolean[]>([])
|
||||||
|
const [pickerVisible, setPickerVisible] = useState<boolean>(false)
|
||||||
|
const [date, setDate] = useState<Date>(
|
||||||
|
props.project?.date ? new Date(props.project.date) : today
|
||||||
|
)
|
||||||
|
const [month, setMonth] = useState<string>(format(today, 'MMMM yyyy'))
|
||||||
|
|
||||||
const urlRegex = useMemo(
|
const urlRegex = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -51,7 +60,7 @@ const ProjectForm = (props: ProjectFormProps) => {
|
|||||||
if (emptyCount > 0) return toast.error(`${emptyCount} links empty`)
|
if (emptyCount > 0) return toast.error(`${emptyCount} links empty`)
|
||||||
|
|
||||||
data.links = links
|
data.links = links
|
||||||
data.date = new Date().toISOString() // TODO: change to date picker value
|
data.date = date.toISOString()
|
||||||
props.onSave(data, props?.project?.id)
|
props.onSave(data, props?.project?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,14 +124,58 @@ const ProjectForm = (props: ProjectFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<FormTextList
|
<div className="form-control w-full">
|
||||||
name="Links"
|
<Label
|
||||||
itemPlaceholder="URL"
|
name="date"
|
||||||
icon={mdiLinkVariant}
|
className="input input-bordered flex items-center gap-2"
|
||||||
list={links}
|
errorClassName="input input-bordered flex items-center gap-2 input-error"
|
||||||
errors={linkErrors}
|
onClick={() => setPickerVisible(!pickerVisible)}
|
||||||
setList={setLinks}
|
>
|
||||||
/>
|
<Label
|
||||||
|
name="date"
|
||||||
|
className="size-4 opacity-70"
|
||||||
|
errorClassName="size-4 text-error"
|
||||||
|
>
|
||||||
|
<Icon path={mdiCalendar} />
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="date"
|
||||||
|
ref={titleRef}
|
||||||
|
placeholder="Date"
|
||||||
|
value={format(date, 'PP')}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pickerVisible && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<DatePicker
|
||||||
|
date={date}
|
||||||
|
setDate={setDate}
|
||||||
|
month={month}
|
||||||
|
setMonth={setMonth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`${!pickerVisible && 'pt-2'}`}>
|
||||||
|
<FormTextList
|
||||||
|
name="Links"
|
||||||
|
itemPlaceholder="URL"
|
||||||
|
icon={mdiLinkVariant}
|
||||||
|
list={links}
|
||||||
|
errors={linkErrors}
|
||||||
|
setList={setLinks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAfter(date, today) && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<p>Project will be marked as</p>
|
||||||
|
<div className="ml-1 badge badge-info">planned</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<nav className="my-2 flex justify-center space-x-2">
|
<nav className="my-2 flex justify-center space-x-2">
|
||||||
<Submit
|
<Submit
|
||||||
|
|||||||
@@ -86,9 +86,15 @@ const ProjectsList = ({ projects }: FindProjects) => {
|
|||||||
<td>{timeTag(project.date)}</td>
|
<td>{timeTag(project.date)}</td>
|
||||||
<td className="space-x-2 space-y-2">
|
<td className="space-x-2 space-y-2">
|
||||||
{project.links.map((link, i) => (
|
{project.links.map((link, i) => (
|
||||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
className="badge badge-ghost text-nowrap"
|
||||||
|
key={i}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
{link}
|
{link}
|
||||||
</div>
|
</a>
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
mdiRename,
|
mdiRename,
|
||||||
mdiAt,
|
mdiAt,
|
||||||
mdiLinkVariant,
|
mdiLinkVariant,
|
||||||
|
mdiPound,
|
||||||
|
mdiAccountPlus,
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import Icon from '@mdi/react'
|
import Icon from '@mdi/react'
|
||||||
import type { EditSocialById, UpdateSocialInput } from 'types/graphql'
|
import type { EditSocialById, UpdateSocialInput } from 'types/graphql'
|
||||||
@@ -32,12 +34,23 @@ const types: FormSocial['type'][] = [
|
|||||||
'facebook',
|
'facebook',
|
||||||
'tiktok',
|
'tiktok',
|
||||||
'youtube',
|
'youtube',
|
||||||
|
'steam',
|
||||||
|
'discord',
|
||||||
|
'twitch',
|
||||||
'linkedin',
|
'linkedin',
|
||||||
'github',
|
'github',
|
||||||
|
'gitea',
|
||||||
|
'forgejo',
|
||||||
|
'gitlab',
|
||||||
|
'bitbucket',
|
||||||
|
'leetcode',
|
||||||
'email',
|
'email',
|
||||||
|
'phone',
|
||||||
'custom',
|
'custom',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const urlHandles: FormSocial['type'][] = ['custom', 'gitea', 'forgejo']
|
||||||
|
|
||||||
const SocialForm = (props: SocialFormProps) => {
|
const SocialForm = (props: SocialFormProps) => {
|
||||||
const [type, setType] = useState<FormSocial['type']>(
|
const [type, setType] = useState<FormSocial['type']>(
|
||||||
props.social?.type ?? 'x'
|
props.social?.type ?? 'x'
|
||||||
@@ -76,7 +89,7 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
<Form<FormSocial>
|
<Form<FormSocial>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
error={props.error}
|
error={props.error}
|
||||||
className="h-96 max-w-80 space-y-2"
|
className="h-128 max-w-80 space-y-2"
|
||||||
>
|
>
|
||||||
<Label name="name" className="form-control w-full">
|
<Label name="name" className="form-control w-full">
|
||||||
<Label
|
<Label
|
||||||
@@ -128,16 +141,28 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
path={
|
path={
|
||||||
type == 'email'
|
type == 'email'
|
||||||
? mdiAt
|
? mdiAt
|
||||||
: type == 'custom'
|
: type == 'phone'
|
||||||
? mdiLinkVariant
|
? mdiPound
|
||||||
: mdiAccount
|
: urlHandles.includes(type)
|
||||||
|
? mdiLinkVariant
|
||||||
|
: type == 'discord'
|
||||||
|
? mdiAccountPlus
|
||||||
|
: mdiAccount
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<TextField
|
<TextField
|
||||||
name="username"
|
name="username"
|
||||||
placeholder={
|
placeholder={
|
||||||
type == 'custom' ? 'URL' : type == 'email' ? 'Email' : 'Username'
|
urlHandles.includes(type)
|
||||||
|
? 'URL'
|
||||||
|
: type == 'phone'
|
||||||
|
? 'Phone'
|
||||||
|
: type == 'email'
|
||||||
|
? 'Email'
|
||||||
|
: type == 'discord'
|
||||||
|
? 'Invite Code'
|
||||||
|
: 'Username'
|
||||||
}
|
}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={username}
|
defaultValue={username}
|
||||||
@@ -153,10 +178,16 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
value:
|
value:
|
||||||
type == 'email'
|
type == 'email'
|
||||||
? /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
? /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
||||||
: type == 'custom' &&
|
: type == 'phone'
|
||||||
/^(?:(?:(?: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,
|
? /^(\+\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,
|
||||||
message: `Invalid ${
|
message: `Invalid ${
|
||||||
type == 'custom' ? 'URL' : type == 'email' && 'Email'
|
urlHandles.includes(type)
|
||||||
|
? 'URL'
|
||||||
|
: type == 'phone'
|
||||||
|
? 'Phone Number'
|
||||||
|
: type == 'email' && 'Email'
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -167,9 +198,14 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
name="username"
|
name="username"
|
||||||
className="text-xs font-semibold text-error"
|
className="text-xs font-semibold text-error"
|
||||||
/>
|
/>
|
||||||
{type !== 'custom' && type !== 'email' && (
|
{type == 'phone' && (
|
||||||
<span className="label-text-alt">{`${baseUrls[type]}${username}`}</span>
|
<span className="label-text-alt">Format: +1 555-555-5555</span>
|
||||||
)}
|
)}
|
||||||
|
{!urlHandles.includes(type) &&
|
||||||
|
type !== 'phone' &&
|
||||||
|
type !== 'email' && (
|
||||||
|
<span className="label-text-alt">{`${baseUrls[type]}${username}`}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { format } from 'date-fns'
|
||||||
import humanize from 'humanize-string'
|
import humanize from 'humanize-string'
|
||||||
|
|
||||||
const MAX_STRING_LENGTH = 150
|
const MAX_STRING_LENGTH = 150
|
||||||
@@ -41,11 +42,12 @@ export const jsonTruncate = (obj: unknown) => {
|
|||||||
|
|
||||||
export const timeTag = (dateTime?: string) => {
|
export const timeTag = (dateTime?: string) => {
|
||||||
let output: string | JSX.Element = ''
|
let output: string | JSX.Element = ''
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
|
||||||
if (dateTime) {
|
if (dateTime) {
|
||||||
output = (
|
output = (
|
||||||
<time dateTime={dateTime} title={dateTime}>
|
<time dateTime={dateTime} title={dateTime}>
|
||||||
{new Date(dateTime).toUTCString()}
|
{format(date, 'PPpp')}
|
||||||
</time>
|
</time>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,38 @@ import {
|
|||||||
SiYoutube,
|
SiYoutube,
|
||||||
SiLinkedin,
|
SiLinkedin,
|
||||||
SiGithub,
|
SiGithub,
|
||||||
|
SiGitea,
|
||||||
|
SiLeetcode,
|
||||||
|
SiBitbucket,
|
||||||
|
SiDiscord,
|
||||||
|
SiForgejo,
|
||||||
|
SiGitlab,
|
||||||
|
SiSteam,
|
||||||
|
SiTwitch,
|
||||||
} from '@icons-pack/react-simple-icons'
|
} from '@icons-pack/react-simple-icons'
|
||||||
import { mdiEmail, mdiLink } from '@mdi/js'
|
import { mdiEmail, mdiLink, mdiPhone } from '@mdi/js'
|
||||||
import Icon from '@mdi/react'
|
import Icon from '@mdi/react'
|
||||||
import type { Handle } from 'types/graphql'
|
import type { Handle } from 'types/graphql'
|
||||||
|
|
||||||
export const baseUrls: Record<Handle, string> = {
|
export const baseUrls: Record<Handle, string> = {
|
||||||
x: 'https://x.com/',
|
x: 'https://x.com/',
|
||||||
facebook: 'https://www.facebook.com/',
|
|
||||||
github: 'https://github.com/',
|
|
||||||
instagram: 'https://www.instagram.com/',
|
|
||||||
linkedin: 'https://www.linkedin.com/in/',
|
|
||||||
threads: 'https://www.threads.net/@',
|
threads: 'https://www.threads.net/@',
|
||||||
|
instagram: 'https://www.instagram.com/',
|
||||||
|
facebook: 'https://www.facebook.com/',
|
||||||
tiktok: 'https://www.tiktok.com/@',
|
tiktok: 'https://www.tiktok.com/@',
|
||||||
youtube: 'https://www.youtube.com/@',
|
youtube: 'https://www.youtube.com/@',
|
||||||
|
steam: 'https://steamcommunity.com/id/',
|
||||||
|
discord: 'https://discord.gg/',
|
||||||
|
twitch: 'https://www.twitch.tv/',
|
||||||
|
linkedin: 'https://www.linkedin.com/in/',
|
||||||
|
github: 'https://github.com/',
|
||||||
|
gitea: '',
|
||||||
|
forgejo: '',
|
||||||
|
gitlab: 'https://gitlab.com/',
|
||||||
|
bitbucket: 'https://bitbucket.org/',
|
||||||
|
leetcode: 'https://leetcode.com/u/',
|
||||||
email: 'mailto:',
|
email: 'mailto:',
|
||||||
|
phone: 'tel:',
|
||||||
custom: '',
|
custom: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +55,24 @@ const logoComponents: Record<Handle, ReactElement> = {
|
|||||||
),
|
),
|
||||||
tiktok: <SiTiktok className="text-tiktok-light dark:text-tiktok-dark" />,
|
tiktok: <SiTiktok className="text-tiktok-light dark:text-tiktok-dark" />,
|
||||||
youtube: <SiYoutube className="text-youtube-light dark:text-youtube-dark" />,
|
youtube: <SiYoutube className="text-youtube-light dark:text-youtube-dark" />,
|
||||||
|
steam: <SiSteam className="text-steam-light dark:text-steam-dark" />,
|
||||||
|
discord: <SiDiscord className="text-discord-light dark:text-discord-dark" />,
|
||||||
|
twitch: <SiTwitch className="text-twitch-light dark:text-twitch-dark" />,
|
||||||
linkedin: (
|
linkedin: (
|
||||||
<SiLinkedin className="text-linkedin-light dark:text-linkedin-dark" />
|
<SiLinkedin className="text-linkedin-light dark:text-linkedin-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" />,
|
||||||
|
forgejo: <SiForgejo className="text-forgejo-light dark:text-forgejo-dark" />,
|
||||||
|
gitlab: <SiGitlab className="text-gitlab-light dark:text-gitlab-dark" />,
|
||||||
|
bitbucket: (
|
||||||
|
<SiBitbucket className="text-bitbucket-light dark:text-bitbucket-dark" />
|
||||||
|
),
|
||||||
|
leetcode: (
|
||||||
|
<SiLeetcode className="text-leetcode-light dark:text-leetcode-dark" />
|
||||||
|
),
|
||||||
email: <Icon path={mdiEmail} className="size-7" />,
|
email: <Icon path={mdiEmail} className="size-7" />,
|
||||||
|
phone: <Icon path={mdiPhone} className="size-7" />,
|
||||||
custom: <Icon path={mdiLink} className="size-7" />,
|
custom: <Icon path={mdiLink} className="size-7" />,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8821,6 +8821,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"date-fns@npm:^4.1.0":
|
||||||
|
version: 4.1.0
|
||||||
|
resolution: "date-fns@npm:4.1.0"
|
||||||
|
checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"debounce@npm:^1.2.0":
|
"debounce@npm:^1.2.0":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "debounce@npm:1.2.1"
|
resolution: "debounce@npm:1.2.1"
|
||||||
@@ -18243,6 +18250,7 @@ __metadata:
|
|||||||
"@uppy/tus": "npm:^4.0.0"
|
"@uppy/tus": "npm:^4.0.0"
|
||||||
autoprefixer: "npm:^10.4.20"
|
autoprefixer: "npm:^10.4.20"
|
||||||
daisyui: "npm:^4.12.10"
|
daisyui: "npm:^4.12.10"
|
||||||
|
date-fns: "npm:^4.1.0"
|
||||||
humanize-string: "npm:2.1.0"
|
humanize-string: "npm:2.1.0"
|
||||||
postcss: "npm:^8.4.41"
|
postcss: "npm:^8.4.41"
|
||||||
postcss-loader: "npm:^8.1.1"
|
postcss-loader: "npm:^8.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user