feat: add project link modal and redirect functionality
This commit is contained in:
@ -23,6 +23,7 @@ interface Project {
|
|||||||
name: string;
|
name: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
}>;
|
}>;
|
||||||
|
link?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTagIcon = (tag: string) => {
|
const getTagIcon = (tag: string) => {
|
||||||
@ -71,6 +72,7 @@ const projects: Project[] = [
|
|||||||
{ name: "Tailwind CSS", icon: getTagIcon("Tailwind CSS") },
|
{ name: "Tailwind CSS", icon: getTagIcon("Tailwind CSS") },
|
||||||
{ name: "TypeScript", icon: getTagIcon("TypeScript") },
|
{ name: "TypeScript", icon: getTagIcon("TypeScript") },
|
||||||
],
|
],
|
||||||
|
link: "https://ventry.host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "ventry.host",
|
title: "ventry.host",
|
||||||
@ -117,15 +119,15 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
|
|
||||||
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
||||||
<FadeIn>
|
<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
|
<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
|
Portfolio
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-baseline gap-4">
|
<div class="flex items-baseline gap-4">
|
||||||
<h2
|
<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
|
Selected Work
|
||||||
</h2>
|
</h2>
|
||||||
@ -134,20 +136,38 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
|
||||||
<div class="grid gap-20 sm:gap-28">
|
<div class="grid gap-24 sm:gap-32">
|
||||||
{
|
{
|
||||||
sortedProjects.map((project, index) => (
|
sortedProjects.map((project, index) => (
|
||||||
<FadeIn delay={index * 0.1}>
|
<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="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}
|
{project.year}
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px grow theme-border opacity-30" />
|
<div class="h-px grow theme-border opacity-30" />
|
||||||
</div>
|
</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="pt-16 sm:pt-20 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 sm:gap-20">
|
||||||
<div class="space-y-4 sm:space-y-6">
|
<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">
|
<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}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
@ -156,23 +176,20 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 sm:space-y-10">
|
<div class="space-y-8 sm:space-y-12">
|
||||||
<div class="space-y-4 sm:space-y-6">
|
<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">
|
<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
|
Technologies
|
||||||
</h4>
|
</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) => {
|
{project.tags.map((tag, tagIndex) => {
|
||||||
const Icon = tag.icon;
|
const Icon = tag.icon;
|
||||||
return (
|
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">
|
<span class="mr-2 flex items-center">
|
||||||
<Icon className="text-lg" />
|
<Icon className="text-lg" />
|
||||||
</span>
|
</span>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
{tagIndex !== project.tags.length - 1 && (
|
|
||||||
<span class="ml-2 opacity-0">•</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -181,7 +198,15 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
))
|
))
|
||||||
@ -189,3 +214,132 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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