Public facing projects showcase, individual project details not done yet
This commit is contained in:
2
api/db/migrations/20240929164343_/migration.sql
Normal file
2
api/db/migrations/20240929164343_/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Project" ALTER COLUMN "images" SET DEFAULT ARRAY[]::TEXT[];
|
@ -69,7 +69,7 @@ model Project {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
description String @default("No description provided")
|
description String @default("No description provided")
|
||||||
images String[]
|
images String[] @default([])
|
||||||
date DateTime
|
date DateTime
|
||||||
links String[] @default([])
|
links String[] @default([])
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
|
@ -10,8 +10,8 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
projects: [Project!]! @requireAuth
|
projects: [Project!]! @skipAuth
|
||||||
project(id: Int!): Project @requireAuth
|
project(id: Int!): Project @skipAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateProjectInput {
|
input CreateProjectInput {
|
||||||
|
@ -7,8 +7,8 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
tags: [Tag!]! @requireAuth
|
tags: [Tag!]! @skipAuth
|
||||||
tag(id: Int!): Tag @requireAuth
|
tag(id: Int!): Tag @skipAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateTagInput {
|
input CreateTagInput {
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"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",
|
||||||
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -30,8 +30,8 @@ const Routes = () => {
|
|||||||
<Set wrap={ScaffoldLayout} title="Projects" titleTo="projects" buttonLabel="New Project" buttonTo="newProject">
|
<Set wrap={ScaffoldLayout} title="Projects" titleTo="projects" buttonLabel="New Project" buttonTo="newProject">
|
||||||
<Route path="/admin/projects/new" page={ProjectNewProjectPage} name="newProject" />
|
<Route path="/admin/projects/new" page={ProjectNewProjectPage} name="newProject" />
|
||||||
<Route path="/admin/projects/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" />
|
<Route path="/admin/projects/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" />
|
||||||
<Route path="/admin/projects/{id:Int}" page={ProjectProjectPage} name="project" />
|
<Route path="/admin/projects/{id:Int}" page={ProjectProjectPage} name="adminProject" />
|
||||||
<Route path="/admin/projects" page={ProjectProjectsPage} name="projects" />
|
<Route path="/admin/projects" page={ProjectProjectsPage} name="adminProjects" />
|
||||||
</Set>
|
</Set>
|
||||||
</PrivateSet>
|
</PrivateSet>
|
||||||
|
|
||||||
@ -49,6 +49,8 @@ const Routes = () => {
|
|||||||
|
|
||||||
<Set wrap={NavbarLayout}>
|
<Set wrap={NavbarLayout}>
|
||||||
<Route path="/" page={HomePage} name="home" />
|
<Route path="/" page={HomePage} name="home" />
|
||||||
|
<Route path="/projects" page={ProjectsPage} name="projects" />
|
||||||
|
<Route path="/project/{id:Int}" page={ProjectPage} name="project" />
|
||||||
<Route path="/contact" page={ContactPage} name="contact" />
|
<Route path="/contact" page={ContactPage} name="contact" />
|
||||||
</Set>
|
</Set>
|
||||||
|
|
||||||
|
56
web/src/components/AutoCarousel/AutoCarousel.tsx
Normal file
56
web/src/components/AutoCarousel/AutoCarousel.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
|
const SCROLL_INTERVAL_SECONDS = 3
|
||||||
|
|
||||||
|
interface AutoCarouselProps {
|
||||||
|
images: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoCarousel = ({ images }: AutoCarouselProps) => {
|
||||||
|
const [activeItem, setActiveItem] = useState<number>(0)
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const scroll = useCallback(() => {
|
||||||
|
setActiveItem((prev) => {
|
||||||
|
if (images.length - 1 > prev) return prev + 1
|
||||||
|
else return 0
|
||||||
|
})
|
||||||
|
}, [images.length])
|
||||||
|
|
||||||
|
const autoScroll = useCallback(
|
||||||
|
() => setInterval(scroll, SCROLL_INTERVAL_SECONDS * 1000),
|
||||||
|
[scroll]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const play = autoScroll()
|
||||||
|
return () => clearInterval(play)
|
||||||
|
}, [autoScroll])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const width = ref.current?.getBoundingClientRect().width
|
||||||
|
ref.current?.scroll({ left: activeItem * (width || 0) })
|
||||||
|
}, [activeItem])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="carousel carousel-center p-2 space-x-2 rounded-box"
|
||||||
|
>
|
||||||
|
{images.map((image, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="carousel-item w-full h-fit my-auto justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`${i}`}
|
||||||
|
className="object-contain rounded-xl size-fit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCarousel
|
@ -61,7 +61,7 @@ export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
|
|||||||
{
|
{
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Project updated')
|
toast.success('Project updated')
|
||||||
navigate(routes.projects())
|
navigate(routes.adminProjects())
|
||||||
},
|
},
|
||||||
onError: (error) => toast.error(error.message),
|
onError: (error) => toast.error(error.message),
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ const NewProject = () => {
|
|||||||
{
|
{
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Project created')
|
toast.success('Project created')
|
||||||
navigate(routes.projects())
|
navigate(routes.adminProjects())
|
||||||
},
|
},
|
||||||
onError: (error) => toast.error(error.message),
|
onError: (error) => toast.error(error.message),
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ const Project = ({ project }: Props) => {
|
|||||||
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
|
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Project deleted')
|
toast.success('Project deleted')
|
||||||
navigate(routes.projects())
|
navigate(routes.adminProjects())
|
||||||
},
|
},
|
||||||
onError: (error) => toast.error(error.message),
|
onError: (error) => toast.error(error.message),
|
||||||
})
|
})
|
||||||
@ -99,7 +99,7 @@ const Project = ({ project }: Props) => {
|
|||||||
{project.tags.map((tag, i) => (
|
{project.tags.map((tag, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="badge"
|
className="badge whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
color:
|
color:
|
||||||
|
@ -235,30 +235,33 @@ const ProjectForm = (props: ProjectFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{fileIds.length > 0 ? (
|
{fileIds.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{fileIds.map((fileId, i) => (
|
{!appendUploader &&
|
||||||
<div
|
fileIds.map((fileId, i) => (
|
||||||
key={i}
|
<div key={i} className="flex justify-center">
|
||||||
className="card rounded-xl image-full image-full-no-overlay"
|
<div className="card rounded-xl w-fit image-full image-full-no-overlay">
|
||||||
>
|
<figure>
|
||||||
<figure>
|
<img src={fileId} alt={i.toString()} />
|
||||||
<img src={fileId} alt={i.toString()} />
|
</figure>
|
||||||
</figure>
|
<div className="card-body p-2 rounded-xl">
|
||||||
<div className="card-body p-2 rounded-xl">
|
<div className="card-actions rounded-md justify-end">
|
||||||
<div className="card-actions rounded-md justify-end">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="btn btn-square btn-sm shadow-xl"
|
||||||
className="btn btn-square btn-sm shadow-xl"
|
onClick={() => {
|
||||||
onClick={() => {
|
setToDelete([...toDelete, fileId])
|
||||||
setToDelete([...toDelete, fileId])
|
setFileIds(fileIds.filter((id) => id !== fileId))
|
||||||
setFileIds(fileIds.filter((id) => id !== fileId))
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Icon
|
||||||
<Icon path={mdiDelete} className="size-4 text-error" />
|
path={mdiDelete}
|
||||||
</button>
|
className="size-4 text-error"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
{appendUploader && (
|
{appendUploader && (
|
||||||
<Uploader
|
<Uploader
|
||||||
onComplete={onUploadComplete}
|
onComplete={onUploadComplete}
|
||||||
|
@ -61,7 +61,7 @@ const ProjectsList = ({ projects }: FindProjects) => {
|
|||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={routes.project({ id: project.id })}
|
to={routes.adminProject({ id: project.id })}
|
||||||
title={'Show project ' + project.id + ' detail'}
|
title={'Show project ' + project.id + ' detail'}
|
||||||
className="btn btn-xs uppercase"
|
className="btn btn-xs uppercase"
|
||||||
>
|
>
|
||||||
@ -96,7 +96,7 @@ const ProjectsList = ({ projects }: FindProjects) => {
|
|||||||
{project.tags.map((tag, i) => (
|
{project.tags.map((tag, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="badge"
|
className="badge whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
color:
|
color:
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { format, isAfter, startOfToday } from 'date-fns'
|
||||||
|
import { FindProjects } from 'types/graphql'
|
||||||
|
|
||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
import AutoCarousel from 'src/components/AutoCarousel/AutoCarousel'
|
||||||
|
import { calculateLuminance } from 'src/lib/color'
|
||||||
|
|
||||||
|
const ProjectsShowcase = ({ projects }: FindProjects) => (
|
||||||
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
|
{projects
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1))
|
||||||
|
.map((project, i) => (
|
||||||
|
<Link key={i} to={routes.project({ id: project.id })}>
|
||||||
|
<div className="card card-compact bg-base-100 w-96 h-fit shadow-xl transition-all hover:-translate-y-2 hover:shadow-2xl">
|
||||||
|
{project.images.length > 0 && (
|
||||||
|
<AutoCarousel images={project.images} />
|
||||||
|
)}
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="card-title overflow-auto">
|
||||||
|
<p className="whitespace-nowrap">{project.title}</p>
|
||||||
|
</div>
|
||||||
|
<div className="line-clamp-5">{project.description}</div>
|
||||||
|
<div className="card-actions justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isAfter(new Date(project.date), startOfToday()) && (
|
||||||
|
<div className="badge badge-info">planned</div>
|
||||||
|
)}
|
||||||
|
<div className="badge badge-ghost">
|
||||||
|
{format(project.date, 'yyyy-MM-dd')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.tags.map((tag, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="badge whitespace-nowrap"
|
||||||
|
style={{
|
||||||
|
backgroundColor: tag.color,
|
||||||
|
color:
|
||||||
|
calculateLuminance(tag.color) > 0.5
|
||||||
|
? 'black'
|
||||||
|
: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag.tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ProjectsShowcase
|
@ -0,0 +1,44 @@
|
|||||||
|
import type { FindProjects, FindProjectsVariables } 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 ProjectsShowcase from '../ProjectsShowcase/ProjectsShowcase'
|
||||||
|
|
||||||
|
export const QUERY: TypedDocumentNode<FindProjects, FindProjectsVariables> =
|
||||||
|
gql`
|
||||||
|
query FindProjects {
|
||||||
|
projects {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
images
|
||||||
|
date
|
||||||
|
links
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
tag
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Loading = () => <CellLoading />
|
||||||
|
export const Empty = () => <CellEmpty />
|
||||||
|
export const Failure = ({ error }: CellFailureProps<FindProjectsVariables>) => (
|
||||||
|
<CellFailure error={error} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Success = ({
|
||||||
|
projects,
|
||||||
|
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
|
||||||
|
<ProjectsShowcase projects={projects} />
|
||||||
|
)
|
@ -65,7 +65,7 @@ const Tag = ({ tag }: Props) => {
|
|||||||
<th>Color</th>
|
<th>Color</th>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="badge"
|
className="badge whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
color:
|
color:
|
||||||
|
@ -96,7 +96,7 @@ const TagsList = ({ tags }: FindTags) => {
|
|||||||
<td>{truncate(tag.tag)}</td>
|
<td>{truncate(tag.tag)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="badge"
|
className="badge whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
color:
|
color:
|
||||||
|
@ -20,6 +20,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
const { isAuthenticated, logOut } = useAuth()
|
const { isAuthenticated, logOut } = useAuth()
|
||||||
|
|
||||||
const navbarRoutes: NavbarRoute[] = [
|
const navbarRoutes: NavbarRoute[] = [
|
||||||
|
{
|
||||||
|
name: 'Projects',
|
||||||
|
path: routes.projects(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Contact',
|
name: 'Contact',
|
||||||
path: routes.contact(),
|
path: routes.contact(),
|
||||||
@ -33,7 +37,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Projects',
|
name: 'Projects',
|
||||||
path: routes.projects(),
|
path: routes.adminProjects(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tags',
|
name: 'Tags',
|
||||||
|
27
web/src/pages/ProjectPage/ProjectPage.tsx
Normal file
27
web/src/pages/ProjectPage/ProjectPage.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
|
interface ProjectPageProps {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
|
||||||
|
const ProjectPage = ({ id }: ProjectPageProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Metadata title="Project" />
|
||||||
|
|
||||||
|
<h1>ProjectPage</h1>
|
||||||
|
<p>
|
||||||
|
Find me in <code>./web/src/pages/ProjectPage/ProjectPage.tsx</code>
|
||||||
|
</p>
|
||||||
|
<p>My id is: {id}</p>
|
||||||
|
{/*
|
||||||
|
My default route is named `project`, link to me with:
|
||||||
|
`<Link to={routes.project()}>Project</Link>`
|
||||||
|
*/}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectPage
|
29
web/src/pages/ProjectsPage/ProjectsPage.tsx
Normal file
29
web/src/pages/ProjectsPage/ProjectsPage.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { isMobile, isBrowser } from 'react-device-detect'
|
||||||
|
|
||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
|
import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell'
|
||||||
|
|
||||||
|
const ProjectsPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Metadata title="Projects" />
|
||||||
|
|
||||||
|
<div className="hero min-h-48">
|
||||||
|
<div className="hero-content">
|
||||||
|
<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
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProjectsShowcaseCell />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectsPage
|
22
yarn.lock
22
yarn.lock
@ -15476,6 +15476,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-dom@npm:18.3.1":
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
resolution: "react-dom@npm:18.3.1"
|
resolution: "react-dom@npm:18.3.1"
|
||||||
@ -17667,6 +17679,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"ua-parser-js@npm:^1.0.35":
|
||||||
version: 1.0.38
|
version: 1.0.38
|
||||||
resolution: "ua-parser-js@npm:1.0.38"
|
resolution: "ua-parser-js@npm:1.0.38"
|
||||||
@ -18167,6 +18188,7 @@ __metadata:
|
|||||||
postcss-loader: "npm:^8.1.1"
|
postcss-loader: "npm:^8.1.1"
|
||||||
react: "npm:18.3.1"
|
react: "npm:18.3.1"
|
||||||
react-colorful: "npm:^5.6.1"
|
react-colorful: "npm:^5.6.1"
|
||||||
|
react-device-detect: "npm:^2.2.3"
|
||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
tailwindcss: "npm:^3.4.8"
|
tailwindcss: "npm:^3.4.8"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
Reference in New Issue
Block a user