feat: project improvements
This commit is contained in:
parent
58f374bda2
commit
19afe08bba
3 changed files with 220 additions and 21 deletions
152
src/app/page.tsx
152
src/app/page.tsx
|
@ -1,26 +1,154 @@
|
||||||
import React from 'react'
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
import { Project } from '@/typings/project'
|
import { Project } from '@/typings/project'
|
||||||
import { ProjectCard } from '@/components/ProjectCard'
|
import { ProjectCard } from '@/components/ProjectCard'
|
||||||
|
|
||||||
export default function Page() {
|
const Page = () => {
|
||||||
const projects: Project[] = [
|
const projects: Project[] = [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'Project Alpha',
|
||||||
description: 'test',
|
description: 'Description for Project Alpha',
|
||||||
category: 'Misc',
|
category: 'Web',
|
||||||
imageUrl: 'https://avatars.githubusercontent.com/u/48355802?v=4',
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
socials: {},
|
socials: { Github: 'https://github.com', YouTube: 'https://youtube.com' },
|
||||||
languages: ['Rust'],
|
languages: ['Java', 'TypeScript'],
|
||||||
|
tags: ['Frontend', 'Backend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Project Beta',
|
||||||
|
description: 'Description for Project Beta',
|
||||||
|
category: 'Minecraft',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: { Github: 'https://github.com' },
|
||||||
|
languages: ['Rust', 'Go'],
|
||||||
tags: ['1.8', 'Fullstack'],
|
tags: ['1.8', 'Fullstack'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Project Gamma',
|
||||||
|
description: 'Description for Project Gamma',
|
||||||
|
category: 'Discord',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: { YouTube: 'https://youtube.com' },
|
||||||
|
languages: ['TypeScript'],
|
||||||
|
tags: ['Backend', 'Archived'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Project Delta',
|
||||||
|
description: 'Description for Project Delta',
|
||||||
|
category: 'Unity',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: {},
|
||||||
|
languages: ['Java'],
|
||||||
|
tags: ['1.17', 'Frontend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Project Epsilon',
|
||||||
|
description: 'Description for Project Epsilon',
|
||||||
|
category: 'Misc',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: { Github: 'https://github.com', YouTube: 'https://youtube.com' },
|
||||||
|
languages: ['Go', 'Rust'],
|
||||||
|
tags: ['Fullstack', 'Paper'],
|
||||||
|
},
|
||||||
|
// Add more projects to test pagination
|
||||||
|
{
|
||||||
|
name: 'Project Zeta',
|
||||||
|
description: 'Description for Project Zeta',
|
||||||
|
category: 'Misc',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: { Github: 'https://github.com', YouTube: 'https://youtube.com' },
|
||||||
|
languages: ['Go', 'Rust'],
|
||||||
|
tags: ['Fullstack', 'Paper'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Project Eta',
|
||||||
|
description: 'Description for Project Eta',
|
||||||
|
category: 'Misc',
|
||||||
|
imageUrl: 'https://via.placeholder.com/150',
|
||||||
|
socials: { Github: 'https://github.com', YouTube: 'https://youtube.com' },
|
||||||
|
languages: ['Go', 'Rust'],
|
||||||
|
tags: ['Fullstack', 'Paper'],
|
||||||
|
},
|
||||||
|
// Add as many projects as needed to test
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [itemsPerPage] = useState(4)
|
||||||
|
const [filterTag, setFilterTag] = useState<string | null>(null)
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
||||||
|
|
||||||
|
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setFilterTag(event.target.value === 'All' ? null : event.target.value)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center p-4 md:p-8">
|
<div className="flex min-h-screen flex-col items-center p-4 md:p-8">
|
||||||
<div>Matz Hilven</div>
|
<div className="mb-8 text-2xl font-bold">Matz Hilven</div>
|
||||||
{projects.map((project: Project, index: number) => {
|
<div className="mb-4 flex gap-4">
|
||||||
return <ProjectCard project={project} key={index} />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
|
|
|
@ -1,15 +1,86 @@
|
||||||
import { Project } from '@/typings/project'
|
import { Project, Tag, Language, Social } from '@/typings/project'
|
||||||
|
import {
|
||||||
|
IconBrandGithub,
|
||||||
|
IconBrandYoutube,
|
||||||
|
IconCoffee,
|
||||||
|
IconBrandRust,
|
||||||
|
IconBrandGolang,
|
||||||
|
IconBrandTypescript,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
project: Project
|
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) => {
|
export const ProjectCard = ({ project }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-80 w-80 transform overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:-translate-y-2">
|
||||||
<div>{project.name}</div>
|
<img
|
||||||
<div>{project.description}</div>
|
src={project.imageUrl}
|
||||||
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
type Social = 'Github' | 'YouTube'
|
export type Social = 'Github' | 'YouTube'
|
||||||
type Language = 'Java' | 'Rust' | 'Go' | 'TypeScript'
|
export type Language = 'Java' | 'Rust' | 'Go' | 'TypeScript'
|
||||||
|
|
||||||
type MinecraftTags = '1.8' | '1.17' | 'Paper' | 'Bungee'
|
type MinecraftTags = '1.8' | '1.17' | 'Paper' | 'Bungee'
|
||||||
type WebTags = 'Frontend' | 'Backend' | 'Fullstack'
|
type WebTags = 'Frontend' | 'Backend' | 'Fullstack'
|
||||||
type Tag = MinecraftTags | WebTags | 'Archived'
|
export type Tag = MinecraftTags | WebTags | 'Archived'
|
||||||
|
|
||||||
type Category = 'Minecraft' | 'Web' | 'Discord' | 'Misc' | 'Unity'
|
export type Category = 'Minecraft' | 'Web' | 'Discord' | 'Misc' | 'Unity'
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
name: string
|
name: string
|
||||||
|
|
Loading…
Reference in a new issue