Compare commits
No commits in common. "38a7e09b689566f3c789504f0a3dcec778b3bf8f" and "58f374bda238c10269bf619c22bbd9218904a1ad" have entirely different histories.
38a7e09b68
...
58f374bda2
6 changed files with 39 additions and 1374 deletions
|
@ -21,8 +21,7 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"sharp": "^0.33.4",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
|
|
|
@ -44,9 +44,6 @@ dependencies:
|
|||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.3)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
|
@ -3642,7 +3639,3 @@ packages:
|
|||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
dev: false
|
||||
|
|
1141
public/projects.json
1141
public/projects.json
File diff suppressed because it is too large
Load diff
123
src/app/page.tsx
123
src/app/page.tsx
|
@ -1,113 +1,26 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Project, Tag, projectsSchema } from '@/typings/project'
|
||||
import React from 'react'
|
||||
import { Project } from '@/typings/project'
|
||||
import { ProjectCard } from '@/components/ProjectCard'
|
||||
import projectsData from '../../public/projects.json'
|
||||
|
||||
const Page = () => {
|
||||
const parsedProjects = projectsSchema.safeParse(projectsData)
|
||||
|
||||
if (!parsedProjects.success) {
|
||||
console.error('Invalid project data', parsedProjects.error)
|
||||
return <div>Error loading projects</div>
|
||||
}
|
||||
|
||||
const projects: Project[] = parsedProjects.data
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(4)
|
||||
const [filterTag, setFilterTag] = useState<Tag | null>(null)
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setFilterTag(
|
||||
event.target.value === 'All' ? null : (event.target.value as Tag)
|
||||
)
|
||||
setCurrentPage(1) // Reset to the first page when filter changes
|
||||
}
|
||||
|
||||
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSortOrder(event.target.value as 'asc' | 'desc')
|
||||
setCurrentPage(1) // Reset to the first page when sort order changes
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const cardWidth = 240 // Width of a single card (adjust as needed)
|
||||
const windowWidth = window.innerWidth
|
||||
const newItemsPerPage = Math.floor(windowWidth / cardWidth)
|
||||
setItemsPerPage(newItemsPerPage > 0 ? newItemsPerPage : 1)
|
||||
}
|
||||
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const filteredProjects = filterTag
|
||||
? projects.filter(project => project.tags.includes(filterTag))
|
||||
: projects
|
||||
|
||||
const sortedProjects = [...filteredProjects].sort((a, b) => {
|
||||
if (sortOrder === 'asc') {
|
||||
return a.name.localeCompare(b.name)
|
||||
} else {
|
||||
return b.name.localeCompare(a.name)
|
||||
}
|
||||
})
|
||||
|
||||
const indexOfLastItem = currentPage * itemsPerPage
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage
|
||||
const currentProjects = sortedProjects.slice(
|
||||
indexOfFirstItem,
|
||||
indexOfLastItem
|
||||
)
|
||||
|
||||
const totalPages = Math.ceil(sortedProjects.length / itemsPerPage)
|
||||
export default function Page() {
|
||||
const projects: Project[] = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'test',
|
||||
category: 'Misc',
|
||||
imageUrl: 'https://avatars.githubusercontent.com/u/48355802?v=4',
|
||||
socials: {},
|
||||
languages: ['Rust'],
|
||||
tags: ['1.8', 'Fullstack'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center p-4 md:p-8">
|
||||
<div className="mb-8 text-2xl font-bold">Matz Hilven</div>
|
||||
<div className="mb-4 flex gap-4">
|
||||
<select onChange={handleFilterChange} className="rounded border p-2">
|
||||
<option value="All">All Tags</option>
|
||||
{[...new Set(projects.flatMap(project => project.tags))].map(tag => (
|
||||
<option key={tag} value={tag}>
|
||||
{tag}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select onChange={handleSortChange} className="rounded border p-2">
|
||||
<option value="asc">Sort by Name (A-Z)</option>
|
||||
<option value="desc">Sort by Name (Z-A)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-4">
|
||||
{currentProjects.map((project: Project, index: number) => (
|
||||
<ProjectCard project={project} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="rounded border px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="rounded border px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div>Matz Hilven</div>
|
||||
{projects.map((project: Project, index: number) => {
|
||||
return <ProjectCard project={project} key={index} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
|
|
@ -1,86 +1,15 @@
|
|||
import { Project, Tag, Language, Social } from '@/typings/project'
|
||||
import {
|
||||
IconBrandGithub,
|
||||
IconBrandYoutube,
|
||||
IconCoffee,
|
||||
IconBrandRust,
|
||||
IconBrandGolang,
|
||||
IconBrandTypescript,
|
||||
} from '@tabler/icons-react'
|
||||
import { Project } from '@/typings/project'
|
||||
|
||||
type Props = {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const socialIcons: Record<Social, React.JSX.Element> = {
|
||||
Github: <IconBrandGithub className="h-5 w-5" />,
|
||||
YouTube: <IconBrandYoutube className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
const languageIcons: Record<Language, React.JSX.Element> = {
|
||||
Java: <IconCoffee className="h-5 w-5" />,
|
||||
Rust: <IconBrandRust className="h-5 w-5" />,
|
||||
Go: <IconBrandGolang className="h-5 w-5" />,
|
||||
TypeScript: <IconBrandTypescript className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
const tagStyles: Record<Tag, string> = {
|
||||
'1.8': 'text-orange-500 border-orange-500',
|
||||
'1.17': 'text-orange-500 border-orange-500',
|
||||
Paper: 'text-blue-500 border-blue-500',
|
||||
Bungee: 'text-blue-500 border-blue-500',
|
||||
Frontend: 'text-cyan-500 border-cyan-500',
|
||||
Backend: 'text-cyan-500 border-cyan-500',
|
||||
Fullstack: 'text-cyan-500 border-cyan-500',
|
||||
Archived: 'text-gray-600 border-gray-600',
|
||||
}
|
||||
|
||||
export const ProjectCard = ({ project }: Props) => {
|
||||
return (
|
||||
<div className="h-80 w-80 transform overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:scale-105">
|
||||
<img
|
||||
src={project.imageUrl}
|
||||
alt={project.name}
|
||||
className="h-40 w-full object-cover"
|
||||
/>
|
||||
<div className="flex flex-grow flex-col justify-between p-4">
|
||||
<div>
|
||||
<div className="mb-2 text-xl font-bold">{project.name}</div>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{project.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`rounded-full border px-2 py-1 text-xs font-semibold ${tagStyles[tag] || 'border-gray-500 text-gray-500'}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">{project.description}</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex space-x-4">
|
||||
{Object.entries(project.socials).map(([social, url]) => (
|
||||
<a
|
||||
key={social}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-700"
|
||||
>
|
||||
{socialIcons[social as Social]}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{project.languages.map(language => (
|
||||
<span key={language} className="text-xl text-gray-700">
|
||||
{languageIcons[language]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{project.name}</div>
|
||||
<div>{project.description}</div>
|
||||
<img src={project.imageUrl} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,46 +1,18 @@
|
|||
import { z } from 'zod'
|
||||
type Social = 'Github' | 'YouTube'
|
||||
type Language = 'Java' | 'Rust' | 'Go' | 'TypeScript'
|
||||
|
||||
export const SocialSchema = z.union([z.literal('Github'), z.literal('YouTube')])
|
||||
export type Social = z.infer<typeof SocialSchema>
|
||||
type MinecraftTags = '1.8' | '1.17' | 'Paper' | 'Bungee'
|
||||
type WebTags = 'Frontend' | 'Backend' | 'Fullstack'
|
||||
type Tag = MinecraftTags | WebTags | 'Archived'
|
||||
|
||||
export const LanguageSchema = z.union([
|
||||
z.literal('Java'),
|
||||
z.literal('Rust'),
|
||||
z.literal('Go'),
|
||||
z.literal('TypeScript'),
|
||||
])
|
||||
export type Language = z.infer<typeof LanguageSchema>
|
||||
type Category = 'Minecraft' | 'Web' | 'Discord' | 'Misc' | 'Unity'
|
||||
|
||||
export const TagSchema = z.union([
|
||||
z.literal('1.8'),
|
||||
z.literal('1.17'),
|
||||
z.literal('Paper'),
|
||||
z.literal('Bungee'),
|
||||
z.literal('Frontend'),
|
||||
z.literal('Backend'),
|
||||
z.literal('Fullstack'),
|
||||
z.literal('Archived'),
|
||||
])
|
||||
export type Tag = z.infer<typeof TagSchema>
|
||||
|
||||
export const CategorySchema = z.union([
|
||||
z.literal('Minecraft'),
|
||||
z.literal('Web'),
|
||||
z.literal('Discord'),
|
||||
z.literal('Misc'),
|
||||
z.literal('Unity'),
|
||||
])
|
||||
export type Category = z.infer<typeof CategorySchema>
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
category: CategorySchema,
|
||||
imageUrl: z.string().url(),
|
||||
socials: z.record(SocialSchema, z.string().url()).optional(),
|
||||
languages: z.array(LanguageSchema),
|
||||
tags: z.array(TagSchema),
|
||||
})
|
||||
export type Project = z.infer<typeof ProjectSchema>
|
||||
|
||||
export const projectsSchema = z.array(ProjectSchema)
|
||||
export type Project = {
|
||||
name: string
|
||||
description: string
|
||||
category: Category
|
||||
imageUrl: string
|
||||
socials: Partial<Record<Social, string>>
|
||||
languages: Language[]
|
||||
tags: Tag[]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue