Project Gallery
A visually appealing project gallery with filtering, sorting, and animated transitions.
Usage
Our Work
Explore our portfolio of successful projects and creative solutions.
E-commerce Platform Redesign
Complete redesign of a major e-commerce platform focusing on user experience and conversion optimization.
Mobile Banking Application
Secure mobile banking application with biometric authentication and real-time transaction monitoring.
Corporate Brand Identity
Comprehensive brand identity package including logo, color palette, typography, and brand guidelines.
Health & Fitness Dashboard
Interactive dashboard for health tracking application with data visualization and personalized insights.
SaaS Analytics Platform
Enterprise analytics platform with customizable dashboards, reporting tools, and real-time data processing.
Restaurant Booking System
Online reservation system for restaurants with table management, waitlist features, and customer notifications.
<template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<MInView
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5 }"
>
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
Our Work
</h1>
</MInView>
<MInView
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5, delay: 0.1 }"
>
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Explore our portfolio of successful projects and creative solutions.
</p>
</MInView>
</div>
<!-- Filters and Search -->
<MInView
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5, delay: 0.2 }"
class="mb-8"
>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<UInput
v-model="searchQuery"
placeholder="Search projects..."
icon="i-heroicons-magnifying-glass"
size="lg"
/>
</div>
<div class="flex gap-2">
<USelectMenu
v-model="categoryFilter"
:items="categories"
value-key="value"
placeholder="All Categories"
size="lg"
/>
<USelectMenu
v-model="sortBy"
:items="sortOptions"
value-key="value"
placeholder="Sort By"
size="lg"
/>
</div>
</div>
</MInView>
<!-- Filter Tags -->
<MInView
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5, delay: 0.3 }"
class="mb-8"
>
<div class="flex flex-wrap gap-2">
<UBadge
v-for="tag in allTags"
:key="tag"
:color="activeTags.includes(tag) ? 'primary' : 'gray'"
variant="soft"
class="cursor-pointer"
@click="toggleTag(tag)"
>
{{ tag }}
</UBadge>
</div>
</MInView>
<!-- Projects Grid -->
<MAnimatedGroup
v-if="filteredProjects.length > 0"
:stagger-children="0.1"
preset="slide"
initial="hidden"
animate="visible"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12"
>
<div
v-for="project in paginatedProjects"
:key="project.id"
>
<UCard
class="h-full flex flex-col group hover:shadow-xl transition-all duration-300 overflow-hidden"
:ui="{ body: { base: 'flex-1 flex flex-col p-6' } }"
>
<div class="relative overflow-hidden rounded-lg mb-4">
<img
:src="project.image"
:alt="project.title"
class="w-full h-48 object-cover transition-transform duration-500 group-hover:scale-105"
>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-300 flex items-center justify-center">
<UButton
:to="`/portfolio/${project.slug}`"
color="white"
variant="solid"
size="sm"
class="opacity-0 group-hover:opacity-100 translate-y-4 group-hover:translate-y-0 transition-all duration-300"
>
View Project
</UButton>
</div>
<div class="absolute top-4 left-4">
<UBadge
:color="getCategoryColor(project.category)"
variant="solid"
>
{{ project.category }}
</UBadge>
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{{ project.title }}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4 flex-1 line-clamp-2">
{{ project.description }}
</p>
<div class="flex flex-wrap gap-1 mb-4">
<UBadge
v-for="tag in project.tags.slice(0, 3)"
:key="tag"
variant="soft"
size="xs"
>
{{ tag }}
</UBadge>
<UBadge
v-if="project.tags.length > 3"
variant="soft"
size="xs"
>
+{{ project.tags.length - 3 }} more
</UBadge>
</div>
</div>
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-800">
<div class="flex items-center">
<UAvatar
:src="project.client.avatar"
:alt="project.client.name"
size="xs"
class="mr-2"
/>
<span class="text-gray-700 dark:text-gray-300 text-sm">{{ project.client.name }}</span>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ project.date }}
</div>
</div>
</UCard>
</div>
</MAnimatedGroup>
<!-- Empty State -->
<MInView
v-if="filteredProjects.length === 0"
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5 }"
class="text-center py-12"
>
<UIcon
name="i-heroicons-photo"
class="w-16 h-16 mx-auto text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No projects found</h3>
<p class="mt-1 text-gray-500 dark:text-gray-400">Try adjusting your search or filter criteria</p>
<UButton
class="mt-6"
@click="resetFilters"
>
Reset Filters
</UButton>
</MInView>
<!-- Pagination -->
<MInView
v-if="filteredProjects.length > 0"
:variants="{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 } }"
:transition="{ duration: 0.5 }"
class="flex justify-center"
>
<UPagination
v-model="page"
:page-count="pageCount"
:total="filteredProjects.length"
/>
</MInView>
</div>
</template>
<script setup lang="ts">
const searchQuery = ref('')
const categoryFilter = ref('all')
const sortBy = ref('date-desc')
const activeTags = ref<string[]>([])
const page = ref(1)
const projectsPerPage = 6
const categories = [
{ label: 'All Categories', value: 'all' },
{ label: 'Web Design', value: 'Web Design' },
{ label: 'Mobile App', value: 'Mobile App' },
{ label: 'Branding', value: 'Branding' },
{ label: 'UI/UX', value: 'UI/UX' },
{ label: 'Development', value: 'Development' }
]
const sortOptions = [
{ label: 'Newest First', value: 'date-desc' },
{ label: 'Oldest First', value: 'date-asc' },
{ label: 'Title A-Z', value: 'title-asc' },
{ label: 'Title Z-A', value: 'title-desc' }
]
const allTags = ['Vue.js', 'React', 'Tailwind CSS', 'Figma', 'Node.js', 'TypeScript', 'GraphQL', 'Firebase', 'Next.js', 'Nuxt.js']
const projects = ref([
{
id: 1,
title: 'E-commerce Platform Redesign',
description: 'Complete redesign of a major e-commerce platform focusing on user experience and conversion optimization.',
category: 'Web Design',
date: 'June 2023',
image: 'https://images.unsplash.com/photo-1551650975-87deedd944c3?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'TechCorp Inc.',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg'
},
tags: ['Vue.js', 'Tailwind CSS', 'Figma', 'Firebase'],
slug: 'ecommerce-platform-redesign'
},
{
id: 2,
title: 'Mobile Banking Application',
description: 'Secure mobile banking application with biometric authentication and real-time transaction monitoring.',
category: 'Mobile App',
date: 'May 2023',
image: 'https://images.unsplash.com/photo-1551836022-d5d88e9218df?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'FinanceGlobal',
avatar: 'https://randomuser.me/api/portraits/women/44.jpg'
},
tags: ['React Native', 'Node.js', 'GraphQL', 'Biometrics'],
slug: 'mobile-banking-application'
},
{
id: 3,
title: 'Corporate Brand Identity',
description: 'Comprehensive brand identity package including logo, color palette, typography, and brand guidelines.',
category: 'Branding',
date: 'April 2023',
image: 'https://images.unsplash.com/photo-1496171367470-9ed9a91ea931?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'StartupXYZ',
avatar: 'https://randomuser.me/api/portraits/men/22.jpg'
},
tags: ['Illustrator', 'Photoshop', 'Brand Guidelines', 'Typography'],
slug: 'corporate-brand-identity'
},
{
id: 4,
title: 'Health & Fitness Dashboard',
description: 'Interactive dashboard for health tracking application with data visualization and personalized insights.',
category: 'UI/UX',
date: 'March 2023',
image: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'Wellness Co.',
avatar: 'https://randomuser.me/api/portraits/women/68.jpg'
},
tags: ['Figma', 'Data Visualization', 'User Research', 'Prototyping'],
slug: 'health-fitness-dashboard'
},
{
id: 5,
title: 'SaaS Analytics Platform',
description: 'Enterprise analytics platform with customizable dashboards, reporting tools, and real-time data processing.',
category: 'Development',
date: 'February 2023',
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'DataInsights Ltd.',
avatar: 'https://randomuser.me/api/portraits/men/56.jpg'
},
tags: ['React', 'Node.js', 'MongoDB', 'D3.js', 'WebSocket'],
slug: 'saas-analytics-platform'
},
{
id: 6,
title: 'Restaurant Booking System',
description: 'Online reservation system for restaurants with table management, waitlist features, and customer notifications.',
category: 'Web Design',
date: 'January 2023',
image: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'Fine Dining Group',
avatar: 'https://randomuser.me/api/portraits/women/32.jpg'
},
tags: ['Vue.js', 'Firebase', 'Twilio', 'Stripe'],
slug: 'restaurant-booking-system'
},
{
id: 7,
title: 'Educational Mobile App',
description: 'Gamified learning platform for children with interactive lessons, progress tracking, and parental controls.',
category: 'Mobile App',
date: 'December 2022',
image: 'https://images.unsplash.com/photo-1536924940846-227afb31e2a5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'EduTech Solutions',
avatar: 'https://randomuser.me/api/portraits/men/78.jpg'
},
tags: ['Flutter', 'Firebase', 'Gamification', 'Animation'],
slug: 'educational-mobile-app'
},
{
id: 8,
title: 'Non-Profit Website Redesign',
description: 'Modern, accessible website for a non-profit organization focused on fundraising and community engagement.',
category: 'Web Design',
date: 'November 2022',
image: 'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
client: {
name: 'Charity Foundation',
avatar: 'https://randomuser.me/api/portraits/women/23.jpg'
},
tags: ['Nuxt.js', 'Tailwind CSS', 'Accessibility', 'Donation Integration'],
slug: 'nonprofit-website-redesign'
}
])
const filteredProjects = computed(() => {
let result = [...projects.value]
// Apply search filter
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(project =>
project.title.toLowerCase().includes(query) ||
project.description.toLowerCase().includes(query) ||
project.category.toLowerCase().includes(query) ||
project.tags.some(tag => tag.toLowerCase().includes(query))
)
}
// Apply category filter
if (categoryFilter.value !== 'all') {
result = result.filter(project => project.category === categoryFilter.value)
}
// Apply tag filters
if (activeTags.value.length > 0) {
result = result.filter(project =>
activeTags.value.every(tag => project.tags.includes(tag))
)
}
// Apply sorting
switch (sortBy.value) {
case 'date-desc':
result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
break
case 'date-asc':
result.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
break
case 'title-asc':
result.sort((a, b) => a.title.localeCompare(b.title))
break
case 'title-desc':
result.sort((a, b) => b.title.localeCompare(a.title))
break
}
return result
})
const pageCount = computed(() => Math.ceil(filteredProjects.value.length / projectsPerPage))
const startIndex = computed(() => (page.value - 1) * projectsPerPage)
const endIndex = computed(() => startIndex.value + projectsPerPage)
const paginatedProjects = computed(() => {
return filteredProjects.value.slice(startIndex.value, endIndex.value)
})
const getCategoryColor = (category: string) => {
switch (category) {
case 'Web Design': return 'blue'
case 'Mobile App': return 'purple'
case 'Branding': return 'pink'
case 'UI/UX': return 'green'
case 'Development': return 'amber'
default: return 'gray'
}
}
const toggleTag = (tag: string) => {
if (activeTags.value.includes(tag)) {
activeTags.value = activeTags.value.filter(t => t !== tag)
} else {
activeTags.value.push(tag)
}
page.value = 1
}
const resetFilters = () => {
searchQuery.value = ''
categoryFilter.value = 'all'
sortBy.value = 'date-desc'
activeTags.value = []
page.value = 1
}
</script>