feat: add project link modal and redirect functionality

This commit is contained in:
2025-05-01 14:18:57 +02:00
parent 1ecdf57df8
commit fc5b43d029

View File

@ -23,6 +23,7 @@ interface Project {
name: string;
icon: any;
}>;
link?: string;
}
const getTagIcon = (tag: string) => {
@ -71,6 +72,7 @@ const projects: Project[] = [
{ name: "Tailwind CSS", icon: getTagIcon("Tailwind CSS") },
{ name: "TypeScript", icon: getTagIcon("TypeScript") },
],
link: "https://ventry.host",
},
{
title: "ventry.host",
@ -117,15 +119,15 @@ const sortedProjects = [...projects].sort((a, b) => {
<div class="max-w-(--breakpoint-xl) mx-auto relative">
<FadeIn>
<div class="flex flex-col gap-3 mb-12 sm:mb-24">
<div class="flex flex-col gap-3 mb-16 sm:mb-28">
<span
class="theme-text-40 uppercase tracking-wider text-sm sm:text-base font-medium font-['Instrument_Sans']"
class="theme-text-40 uppercase tracking-wider text-sm sm:text-base font-medium font-['Instrument_Sans'] relative inline-flex items-center before:content-[''] before:absolute before:-left-8 before:w-6 before:h-px before:theme-border"
>
Portfolio
</span>
<div class="flex items-baseline gap-4">
<h2
class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"
class="font-['DM_Sans'] text-3xl sm:text-5xl lg:text-6xl font-semibold tracking-tight theme-primary"
>
Selected Work
</h2>
@ -134,20 +136,38 @@ const sortedProjects = [...projects].sort((a, b) => {
</div>
</FadeIn>
<div class="grid gap-20 sm:gap-28">
<div class="grid gap-24 sm:gap-32">
{
sortedProjects.map((project, index) => (
<FadeIn delay={index * 0.1}>
<div class="group relative">
<div class="group relative" data-project-card>
{project.link && (
<button
class="project-link absolute inset-0 z-30 cursor-pointer w-full h-full"
data-project-link
data-project-title={project.title}
data-project-url={project.link}
>
<span class="sr-only">View {project.title}</span>
</button>
)}
<div class="absolute top-0 left-0 right-0 flex items-center gap-4">
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4">
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4 flex items-center">
{project.year}
</div>
<div class="h-px grow theme-border opacity-30" />
</div>
<div class="pt-12 sm:pt-16 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-8 sm:gap-16">
<div class="space-y-4 sm:space-y-6">
<div class="pt-16 sm:pt-20 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 sm:gap-20">
<div class="space-y-5 sm:space-y-7">
{project.link && (
<div class="flex items-center">
<span class="text-2xl sm:text-3xl theme-text-40 opacity-60 group-hover:opacity-90 transition-all duration-300">
<project.icon />
</span>
</div>
)}
<h3 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary group-hover:theme-text-90 transition-colors">
{project.title}
</h3>
@ -156,23 +176,20 @@ const sortedProjects = [...projects].sort((a, b) => {
</p>
</div>
<div class="space-y-6 sm:space-y-10">
<div class="space-y-4 sm:space-y-6">
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
<div class="space-y-8 sm:space-y-12">
<div class="space-y-5 sm:space-y-7">
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium flex items-center before:content-[''] before:w-6 before:h-px before:theme-border before:mr-3">
Technologies
</h4>
<div class="flex flex-wrap items-center gap-3 sm:gap-4">
<div class="flex flex-wrap items-center gap-3 sm:gap-4 relative z-20 pointer-events-none">
{project.tags.map((tag, tagIndex) => {
const Icon = tag.icon;
return (
<span class="flex items-center text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-tight font-medium py-1.5 sm:py-2 group-hover:theme-text-90 transition-all duration-300 theme-bg-05 px-4 sm:px-5 rounded-xl shadow-sm border border-transparent group-hover:theme-border">
<span class="flex items-center text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-tight font-medium py-2 px-5 group-hover:theme-text-90 transition-all duration-300 theme-bg-05 rounded-full shadow-sm border border-transparent group-hover:theme-border">
<span class="mr-2 flex items-center">
<Icon className="text-lg" />
</span>
{tag.name}
{tagIndex !== project.tags.length - 1 && (
<span class="ml-2 opacity-0">•</span>
)}
</span>
);
})}
@ -181,7 +198,15 @@ const sortedProjects = [...projects].sort((a, b) => {
</div>
</div>
<div class="absolute -inset-x-6 sm:-inset-x-8 -inset-y-6 sm:-inset-y-8 rounded-2xl sm:rounded-3xl border border-transparent hover:theme-border group-hover:bg-white/[0.01] transition-all duration-300 shadow-sm group-hover:shadow-md" />
<div class="absolute -inset-x-8 sm:-inset-x-12 -inset-y-8 sm:-inset-y-12 rounded-3xl sm:rounded-[2rem] border border-transparent hover:theme-border group-hover:bg-white/[0.02] transition-all duration-300 shadow-sm group-hover:shadow-lg" />
{project.link && (
<div class="absolute bottom-5 right-5 opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-2 group-hover:translate-y-0 z-20 pointer-events-none">
<span class="flex items-center theme-text-90 font-['Instrument_Sans'] text-sm bg-white/10 px-4 py-2 rounded-full shadow-md backdrop-blur-sm">
<MdLaunch className="mr-2" /> View Project
</span>
</div>
)}
</div>
</FadeIn>
))
@ -189,3 +214,132 @@ const sortedProjects = [...projects].sort((a, b) => {
</div>
</div>
</section>
<div id="redirect-modal" class="fixed inset-0 z-50 flex items-center justify-center opacity-0 pointer-events-none transition-all duration-300 ease-out">
<div class="absolute inset-0 bg-black bg-opacity-0 backdrop-blur-[0px] transition-all duration-300 ease-out" id="modal-overlay"></div>
<div class="relative bg-neutral-900 border theme-border rounded-2xl w-full max-w-md p-6 sm:p-8 shadow-xl transform transition-all duration-300 ease-out scale-95 opacity-0">
<div class="text-center">
<span class="inline-block text-4xl theme-text-40 mb-4">
<MdLaunch />
</span>
<h3 class="font-['DM_Sans'] text-2xl sm:text-3xl font-semibold tracking-tight theme-primary mb-2">
External Link
</h3>
<p class="font-['Instrument_Sans'] theme-text-70 mb-6">
You are being redirected to <span id="redirect-url" class="theme-primary font-medium"></span>
</p>
<div class="flex flex-col-reverse sm:flex-row gap-3 sm:gap-4 justify-center">
<button
id="cancel-redirect"
class="theme-primary-gradient text-white py-3 px-6 rounded-xl font-medium hover:opacity-90 transition-all cursor-pointer"
>
Cancel
</button>
<button
id="confirm-redirect"
class="theme-bg-05 theme-text-90 py-3 px-6 rounded-xl font-medium border border-transparent hover:theme-border transition-all cursor-pointer"
>
Continue to Site
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('redirect-modal');
const modalOverlay = document.getElementById('modal-overlay');
const modalContent = modal?.querySelector('.max-w-md');
const cancelButton = document.getElementById('cancel-redirect');
const confirmButton = document.getElementById('confirm-redirect');
const redirectUrlElement = document.getElementById('redirect-url');
let currentUrl = '';
let currentTitle = '';
function showModal(title, url) {
currentUrl = url;
currentTitle = title;
if (redirectUrlElement) {
redirectUrlElement.textContent = new URL(url).hostname;
}
if (modal) {
modal.classList.remove('pointer-events-none');
setTimeout(() => {
modal.classList.add('opacity-100');
if (modalOverlay) {
modalOverlay.classList.add('bg-opacity-60', 'backdrop-blur-sm');
}
if (modalContent) {
modalContent.classList.remove('scale-95', 'opacity-0');
modalContent.classList.add('scale-100', 'opacity-100');
}
}, 10);
document.body.style.overflow = 'hidden';
}
}
function hideModal() {
if (modal) {
modal.classList.remove('opacity-100');
if (modalOverlay) {
modalOverlay.classList.remove('bg-opacity-60', 'backdrop-blur-sm');
}
if (modalContent) {
modalContent.classList.remove('scale-100', 'opacity-100');
modalContent.classList.add('scale-95', 'opacity-0');
}
setTimeout(() => {
modal.classList.add('pointer-events-none');
document.body.style.overflow = '';
}, 300);
}
}
const projectLinks = document.querySelectorAll('[data-project-link]');
projectLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const url = link.getAttribute('data-project-url');
const title = link.getAttribute('data-project-title');
if (url) {
showModal(title, url);
}
});
});
if (cancelButton) {
cancelButton.addEventListener('click', hideModal);
}
if (modalOverlay) {
modalOverlay.addEventListener('click', hideModal);
}
if (confirmButton) {
confirmButton.addEventListener('click', () => {
if (currentUrl) {
window.open(currentUrl, '_blank');
}
hideModal();
});
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideModal();
}
});
});
</script>