diff --git a/app/projects/[category]/[slug]/page.tsx b/app/projects/[category]/[slug]/page.tsx index f44aa92..182c305 100644 --- a/app/projects/[category]/[slug]/page.tsx +++ b/app/projects/[category]/[slug]/page.tsx @@ -16,6 +16,7 @@ import Mermaid from "@/components/Mermaid"; import ProjectShowcase from "@/components/ProjectShowcase"; import ImageCarousel from "@/components/ImageCarousel"; import ReactMarkdown from "react-markdown"; +import MobileStack from "@/components/MobileStack"; export default function ProjectDetail({ params, @@ -123,15 +124,20 @@ export default function ProjectDetail({
- {/* Desktop Showcase View */} -
- -
- - {/* Mobile Carousel View */} -
- -
+ {project.category === "mobile" ? ( + + ) : ( + <> + {/* Display on large screens */} +
+ +
+ {/* Display on small screens */} +
+ +
+ + )}

Interactive Gallery — Select or swipe to explore diff --git a/components/MobileFrame.tsx b/components/MobileFrame.tsx new file mode 100644 index 0000000..b21af63 --- /dev/null +++ b/components/MobileFrame.tsx @@ -0,0 +1,10 @@ +export function MobileFrame({ children }: { children: React.ReactNode }) { + return ( +

+
+
+ {children} +
+
+ ); +} diff --git a/components/MobileStack.tsx b/components/MobileStack.tsx new file mode 100644 index 0000000..901219c --- /dev/null +++ b/components/MobileStack.tsx @@ -0,0 +1,104 @@ +"use client"; +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ArrowRight } from "lucide-react"; +import { MobileFrame } from "./MobileFrame"; + +export default function MobileStack({ images }: { images: string[] }) { + const [currentIndex, setCurrentIndex] = useState(0); + + const DRAG_THRESHOLD = -150; + + const getRelativeIndex = (index: number) => { + const len = images.length; + return (index - currentIndex + len) % len; + }; + + const next = () => setCurrentIndex((prev) => (prev + 1) % images.length); + + return ( +
+
+ + {images.map((img, index) => { + const relIndex = getRelativeIndex(index); + const isTop = relIndex === 0; + + const xOffset = relIndex * 90; + + if (relIndex > 5) return null; + + return ( + { + if (isTop && info.offset.x < DRAG_THRESHOLD) { + next(); + } + }} + onDragEnd={(_, info) => { + // Backup check for quick flicks + if (isTop && info.offset.x < -100) { + next(); + } + }} + onClick={() => !isTop && setCurrentIndex(index)} + className="absolute" + > +
+ + App Screenshot + +
+
+ ); + })} +
+ + {/* Navigation Button */} +
+ +
+
+
+ ); +} diff --git a/data/projects.ts b/data/projects.ts index c4e1e5c..0fe22fb 100644 --- a/data/projects.ts +++ b/data/projects.ts @@ -244,6 +244,82 @@ graph TB classDef node fill:#16a34a,stroke:#22c55e,color:#fff `, }, + { + slug: "choosa", + category: "mobile", + title: "Choosa", + subtitle: "Social Content Discovery Engine", + role: "Lead Developer & Architect", + duration: "2023 — Present", + stack: [ + "Flutter", + "Firebase", + "Firestore", + "Cloud Functions", + "Push Notifications", + ], + metrics: [ + "Real-time Match Engine", + "Cross-Platform (iOS/Android)", + "Multi-API Orchestration", + ], + description: + "A social decision-making app that solves 'choice paralysis' by allowing groups to swipe on movies and TV shows, using a real-time matching algorithm to find common interests.", + storyLabel: "UX // MOBILE_SYNCHRONIZATION", + images: [ + "/projects/choosa/choosa-1.jpg", + "/projects/choosa/choosa-2.jpg", + "/projects/choosa/choosa-3.jpg", + "/projects/choosa/choosa-4.jpg", + "/projects/choosa/choosa-5.jpg", + ], + isPrivate: false, + engineeringStory: ` +Choosa was built to solve the universal problem of "choice paralysis" in social settings. The challenge was creating a low-latency, real-time environment where group preferences could be aggregated and matched instantaneously. + +#### Real-time State & Match Logic +The core engine utilizes **Firestore's** real-time listeners to sync swipe states across multiple devices simultaneously. I architected a custom matching algorithm within **Firebase Cloud Functions** that monitors group sessions; the moment a consensus is reached, the system triggers **Firebase Cloud Messaging (FCM)** to send push notifications to all participants, ensuring a seamless transition from "deciding" to "watching." + +#### Data Orchestration & External APIs +To provide a rich library of content, I integrated the **TMDB** and **Movie of the Night** APIs. By utilizing a middleware layer in Cloud Functions, I was able to normalize data from different sources, filter results based on user-specific streaming subscriptions, and cache results to minimize API overhead and latency. + +#### Mobile Deployment & Native Experience +Developing Choosa in **Flutter** allowed for a unified codebase while maintaining native performance on both iOS and Android. I managed the full deployment lifecycle, from configuring **Fastlane** for automated App Store and Play Store releases to handling platform-specific requirements like adaptive icons and deep-linking. + `, + mermaidChart: ` +graph LR + subgraph Client_Mobile [Mobile Frontend] + A[Flutter App]:::traffic + end + + subgraph Firebase_Core [Backend Services] + Hub((Firebase SDK)):::hub + B[Auth]:::node + C[Firestore DB]:::node + D[Cloud Functions]:::node + E[Cloud Messaging]:::node + end + + subgraph External_Data [Content Providers] + F[TMDB API]:::traffic + G[Movie of Night API]:::traffic + end + + A <--> Hub + Hub --> B + Hub <-->|Sync State| C + Hub -->|Trigger Match| D + D -->|Push Notification| E + E -->|Alert Group| A + + D --> F + D --> G + + classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff + classDef node fill:#16a34a,stroke:#22c55e,color:#fff + classDef hub fill:#f59e0b,stroke:#d97706,color:#fff + `, + }, { slug: "flutter-1", category: "mobile", diff --git a/public/projects/choosa/choosa-1.jpg b/public/projects/choosa/choosa-1.jpg new file mode 100644 index 0000000..ddd6bf9 Binary files /dev/null and b/public/projects/choosa/choosa-1.jpg differ diff --git a/public/projects/choosa/choosa-2.jpg b/public/projects/choosa/choosa-2.jpg new file mode 100644 index 0000000..d1f12bb Binary files /dev/null and b/public/projects/choosa/choosa-2.jpg differ diff --git a/public/projects/choosa/choosa-3.jpg b/public/projects/choosa/choosa-3.jpg new file mode 100644 index 0000000..68dfeb9 Binary files /dev/null and b/public/projects/choosa/choosa-3.jpg differ diff --git a/public/projects/choosa/choosa-4.jpg b/public/projects/choosa/choosa-4.jpg new file mode 100644 index 0000000..c063902 Binary files /dev/null and b/public/projects/choosa/choosa-4.jpg differ diff --git a/public/projects/choosa/choosa-5.jpg b/public/projects/choosa/choosa-5.jpg new file mode 100644 index 0000000..b211e4e Binary files /dev/null and b/public/projects/choosa/choosa-5.jpg differ