improvements
This commit is contained in:
parent
9644884de9
commit
832be077f7
6 changed files with 179 additions and 113 deletions
|
@ -4,25 +4,34 @@ import '@mantine/core/styles.css'
|
|||
import type { Metadata } from 'next'
|
||||
import React from 'react'
|
||||
import { ColorSchemeScript, MantineProvider } from '@mantine/core'
|
||||
|
||||
import { Montserrat } from 'next/font/google'
|
||||
import Navbar from '@/components/Navbar'
|
||||
export const metadata: Metadata = {
|
||||
title: 'Matz Hilven',
|
||||
title: 'Matz Hilven | Software Engineer',
|
||||
description: 'todo',
|
||||
}
|
||||
|
||||
const font = Montserrat({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className={font.className}>
|
||||
<head>
|
||||
<ColorSchemeScript />
|
||||
</head>
|
||||
<body className="dark">
|
||||
<div className="bg-bg">
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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 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 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>
|
||||
)
|
||||
return <div>hey</div>
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
|
99
src/app/projects/page.tsx
Normal file
99
src/app/projects/page.tsx
Normal 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
|
18
src/components/NavLink.tsx
Normal file
18
src/components/NavLink.tsx
Normal 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
39
src/components/Navbar.tsx
Normal 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
|
|
@ -2,21 +2,9 @@ import type { Config } from 'tailwindcss'
|
|||
|
||||
const config = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
prefix: '',
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
dropShadow: {
|
||||
glow: [
|
||||
|
@ -25,6 +13,12 @@ const config = {
|
|||
],
|
||||
},
|
||||
keyframes: {
|
||||
gradient: {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'25%': { backgroundPosition: '100% 0%' },
|
||||
'50%': { backgroundPosition: '100% 100%' },
|
||||
'75%': { backgroundPosition: '0% 100%' },
|
||||
},
|
||||
jump: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
|
@ -32,6 +26,7 @@ const config = {
|
|||
},
|
||||
animation: {
|
||||
jump: 'jump 0.4s ease-in-out',
|
||||
gradient: 'gradient 10s ease infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue