improvements

This commit is contained in:
Matz Hilven 2024-06-10 14:33:20 +02:00
parent 9644884de9
commit 832be077f7
Signed by: MatzHilven
GPG key ID: ACEB669C2CB79EB7
6 changed files with 179 additions and 113 deletions

View file

@ -4,26 +4,35 @@ import '@mantine/core/styles.css'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import React from 'react' import React from 'react'
import { ColorSchemeScript, MantineProvider } from '@mantine/core' import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Montserrat } from 'next/font/google'
import Navbar from '@/components/Navbar'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Matz Hilven', title: 'Matz Hilven | Software Engineer',
description: 'todo', description: 'todo',
} }
const font = Montserrat({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<html lang="en"> <html lang="en" className={font.className}>
<head> <head>
<ColorSchemeScript /> <ColorSchemeScript />
</head> </head>
<body className="dark"> <body className="dark">
<div className="bg-bg"> <div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Navbar />
<div className="flex min-h-screen flex-col items-center p-4 md:p-8">
<MantineProvider>{children}</MantineProvider> <MantineProvider>{children}</MantineProvider>
</div> </div>
</div>
</body> </body>
</html> </html>
) )

View file

@ -1,99 +1,5 @@
'use client'
import React, { useState, useEffect, useMemo } from 'react'
import { ProjectCard } from '@/components/ProjectCard'
import projectsData from '../../public/projects.json'
import { projectsSchema, Project } from '@/typings/project'
import { useInView } from 'react-intersection-observer'
const Page = () => { const Page = () => {
const parsedProjects = projectsSchema.safeParse(projectsData) return <div>hey</div>
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] = useState<number>(12)
const [filterTag, setFilterTag] = useState<string | null>(null)
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
const { ref, inView } = useInView({
threshold: 1.0,
})
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setFilterTag(event.target.value === 'All' ? null : event.target.value)
setCurrentPage(1)
}
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortOrder(event.target.value as 'asc' | 'desc')
setCurrentPage(1)
}
const filteredAndSortedProjects = useMemo(() => {
let updatedProjects = filterTag
? projects.filter(project => project.tags.includes(filterTag))
: projects
updatedProjects = [...updatedProjects].sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name)
} else {
return b.name.localeCompare(a.name)
}
})
return updatedProjects
}, [projects, filterTag, sortOrder])
const visibleProjects = useMemo(() => {
return filteredAndSortedProjects.slice(0, currentPage * itemsPerPage)
}, [filteredAndSortedProjects, currentPage, itemsPerPage])
useEffect(() => {
if (
inView &&
currentPage * itemsPerPage < filteredAndSortedProjects.length
) {
setCurrentPage(prevPage => prevPage + 1)
}
}, [inView, currentPage, itemsPerPage, filteredAndSortedProjects.length])
return (
<div className="flex min-h-screen flex-col items-center bg-white p-4 text-gray-900 dark:bg-gray-900 dark:text-gray-100 md:p-8">
<div className="mb-4 flex gap-4">
<select
onChange={handleFilterChange}
className="rounded border border-gray-300 bg-white p-2 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<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 border-gray-300 bg-white p-2 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<option value="asc">Sort by Name (A-Z)</option>
<option value="desc">Sort by Name (Z-A)</option>
</select>
</div>
<div className="m-8 flex flex-wrap justify-center gap-6">
{visibleProjects.map((project, index) => (
<ProjectCard project={project} key={index} />
))}
</div>
<div ref={ref}></div>
</div>
)
} }
export default Page export default Page

99
src/app/projects/page.tsx Normal file
View file

@ -0,0 +1,99 @@
'use client'
import React, { useState, useEffect, useMemo } from 'react'
import { ProjectCard } from '@/components/ProjectCard'
import projectsData from '../../../public/projects.json'
import { projectsSchema, Project } from '@/typings/project'
import { useInView } from 'react-intersection-observer'
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] = useState<number>(12)
const [filterTag, setFilterTag] = useState<string | null>(null)
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
const { ref, inView } = useInView({
threshold: 1.0,
})
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setFilterTag(event.target.value === 'All' ? null : event.target.value)
setCurrentPage(1)
}
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortOrder(event.target.value as 'asc' | 'desc')
setCurrentPage(1)
}
const filteredAndSortedProjects = useMemo(() => {
let updatedProjects = filterTag
? projects.filter(project => project.tags.includes(filterTag))
: projects
updatedProjects = [...updatedProjects].sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name)
} else {
return b.name.localeCompare(a.name)
}
})
return updatedProjects
}, [projects, filterTag, sortOrder])
const visibleProjects = useMemo(() => {
return filteredAndSortedProjects.slice(0, currentPage * itemsPerPage)
}, [filteredAndSortedProjects, currentPage, itemsPerPage])
useEffect(() => {
if (
inView &&
currentPage * itemsPerPage < filteredAndSortedProjects.length
) {
setCurrentPage(prevPage => prevPage + 1)
}
}, [inView, currentPage, itemsPerPage, filteredAndSortedProjects.length])
return (
<div>
<div className="mb-4 flex gap-4">
<select
onChange={handleFilterChange}
className="rounded border border-gray-300 bg-white p-2 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<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 border-gray-300 bg-white p-2 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<option value="asc">Sort by Name (A-Z)</option>
<option value="desc">Sort by Name (Z-A)</option>
</select>
</div>
<div className="m-8 flex flex-wrap justify-center gap-6">
{visibleProjects.map((project, index) => (
<ProjectCard project={project} key={index} />
))}
</div>
<div ref={ref}></div>
</div>
)
}
export default Page

View file

@ -0,0 +1,18 @@
import Link from 'next/link'
type NavLinkProps = {
href: string
isActive: boolean
label: string
}
const NavLink = ({ href, isActive, label }: NavLinkProps) => {
return (
<div className="drop-shadow-glow group relative text-xl">
<Link href={href}>{label}</Link>
<span className="absolute bottom-[-4px] left-1/2 block h-0.5 w-0 bg-black transition-all duration-300 group-hover:left-0 group-hover:w-full dark:bg-white"></span>
</div>
)
}
export default NavLink

39
src/components/Navbar.tsx Normal file
View file

@ -0,0 +1,39 @@
'use client'
import React from 'react'
import { usePathname } from 'next/navigation'
import NavLink from './NavLink'
import Link from 'next/link'
const routes = [
{ path: '/', label: 'About' },
{ path: '/projects', label: 'Projects' },
{ path: '/homelab', label: 'Homelab' },
{ path: '/contact', label: 'Contact' },
]
const Navbar = () => {
const pathname = usePathname()
return (
<nav className="flex h-16 items-center justify-between shadow-md">
<Link href="/">
<div className="animate-gradient cursor-pointer select-none bg-gradient-to-r from-blue-400 via-pink-500 to-red-500 bg-[length:200%_200%] bg-clip-text pl-4 text-2xl font-semibold uppercase text-transparent">
Matz Hilven
</div>
</Link>
<div className="mr-8 flex items-center space-x-8">
{routes.map(({ path, label }) => (
<NavLink
key={path}
href={path}
isActive={pathname === path}
label={label}
/>
))}
</div>
</nav>
)
}
export default Navbar

View file

@ -2,21 +2,9 @@ import type { Config } from 'tailwindcss'
const config = { const config = {
darkMode: ['class'], darkMode: ['class'],
content: [ content: ['./src/**/*.{ts,tsx}'],
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: '', prefix: '',
theme: { theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: { extend: {
dropShadow: { dropShadow: {
glow: [ glow: [
@ -25,6 +13,12 @@ const config = {
], ],
}, },
keyframes: { keyframes: {
gradient: {
'0%, 100%': { backgroundPosition: '0% 50%' },
'25%': { backgroundPosition: '100% 0%' },
'50%': { backgroundPosition: '100% 100%' },
'75%': { backgroundPosition: '0% 100%' },
},
jump: { jump: {
'0%, 100%': { transform: 'translateY(0)' }, '0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' }, '50%': { transform: 'translateY(-10px)' },
@ -32,6 +26,7 @@ const config = {
}, },
animation: { animation: {
jump: 'jump 0.4s ease-in-out', jump: 'jump 0.4s ease-in-out',
gradient: 'gradient 10s ease infinite',
}, },
}, },
}, },