feat: add project link modal and redirect functionality
This commit is contained in:
@ -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>
|
||||
|
Reference in New Issue
Block a user