Skip to content

Commit

Permalink
Enable projects selection (#370)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Daniel R Farrell <[email protected]>
  • Loading branch information
Kitenite and drfarrell authored Sep 18, 2024
1 parent 0547cff commit e690b41
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 175 deletions.
137 changes: 0 additions & 137 deletions app/src/routes/projects/Carousel.tsx

This file was deleted.

130 changes: 130 additions & 0 deletions app/src/routes/projects/ProjectsTab/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import useEmblaCarousel from 'embla-carousel-react';
import React, { useCallback, useEffect, useState } from 'react';

interface EmblaCarouselProps {
slides: { id: number; imgSrc: string; title: string }[];
onSlideChange: (index: number) => void;
}

const EmblaCarousel: React.FC<EmblaCarouselProps> = ({ slides, onSlideChange }) => {
const [emblaRef, emblaApi] = useEmblaCarousel({
axis: 'y',
loop: false,
align: 'center',
containScroll: 'trimSnaps',
skipSnaps: false,
dragFree: false,
});
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);

const scrollPrev = useCallback(() => emblaApi && emblaApi.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi && emblaApi.scrollNext(), [emblaApi]);

const onSelect = useCallback(() => {
if (!emblaApi) {
return;
}
setPrevBtnEnabled(emblaApi.canScrollPrev());
setNextBtnEnabled(emblaApi.canScrollNext());
setCurrentIndex(emblaApi.selectedScrollSnap());
onSlideChange(emblaApi.selectedScrollSnap());
}, [emblaApi, onSlideChange]);

useEffect(() => {
if (!emblaApi) {
return;
}
onSelect();
emblaApi.on('select', onSelect);
emblaApi.on('reInit', onSelect);
}, [emblaApi, onSelect]);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowDown') {
event.preventDefault();
scrollNext();
}
};

window.addEventListener('keydown', handleKeyDown);

return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [scrollPrev, scrollNext]);

return (
<div
className="embla relative h-[calc(100vh-5.5rem)] overflow-hidden"
style={{ zIndex: 0 }}
>
<div
className="embla__viewport h-full absolute inset-0"
ref={emblaRef}
style={{
transition: 'transform 0.2s cubic-bezier(0.25, 1, 0.5, 1)',
zIndex: -1,
}}
>
<div className="embla__container h-full">
{slides.map((slide, index) => (
<div
key={slide.id}
className="embla__slide h-full relative flex items-center justify-center"
style={{
flex: '0 0 90%',
minWidth: 0,
margin: '0 -5%',
}}
>
<img
src={slide.imgSrc}
alt={slide.title}
className="rounded-lg object-contain transition-all duration-300"
style={{
maxWidth: '580px',
width: '100%',
height: 'auto',
}}
/>
</div>
))}
</div>
</div>
<div className="embla__buttons absolute left-14 top-1/2 transform -translate-y-1/2 flex flex-col gap-4 z-10 items-center">
<button
className="embla__button embla__button--prev"
onClick={scrollPrev}
disabled={!prevBtnEnabled}
>
<ChevronUpIcon
className={`w-7 h-7 ${prevBtnEnabled ? 'text-white' : 'text-gray-400'}`}
/>
</button>
<div className="flex flex-row space-x-1 text-white items-center">
<span className="text-active">{currentIndex + 1}</span>
<span className="text-sm text-gray-500"> of </span>
<span className="text-active text-active">{slides.length}</span>
</div>
<button
className="embla__button embla__button--next"
onClick={scrollNext}
disabled={!nextBtnEnabled}
>
<ChevronDownIcon
className={`w-7 h-7 ${nextBtnEnabled ? 'text-white' : 'text-gray-400'}`}
/>
</button>
</div>
</div>
);
};

export default EmblaCarousel;
81 changes: 81 additions & 0 deletions app/src/routes/projects/ProjectsTab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Button } from '@/components/ui/button';
import { DotsVerticalIcon, Pencil2Icon } from '@radix-ui/react-icons';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import EmblaCarousel from './Carousel';

export function ProjectsTab() {
const [currentSlide, setCurrentSlide] = useState(0);
const [direction, setDirection] = useState(0);

const slides = [
{ id: 0, imgSrc: 'https://picsum.photos/id/237/200/300', title: 'Airbnb.com' },
{ id: 1, imgSrc: 'https://picsum.photos/id/238/300/200', title: 'Netflix Clone' },
{ id: 2, imgSrc: 'https://picsum.photos/id/239/500/500', title: 'Personal Portfolio' },
];

const handleSlideChange = (index: number) => {
if (currentSlide === index) {
return;
}
setDirection(index > currentSlide ? 1 : -1);
setCurrentSlide(index);
};

const variants = {
enter: (direction: number) => ({
y: direction > 0 ? 20 : -20,
opacity: 0,
}),
center: {
y: 0,
opacity: 1,
},
exit: (direction: number) => ({
y: direction < 0 ? 20 : -20,
opacity: 0,
}),
};

return (
<div className="flex h-[calc(100vh-5.5rem)] w-full">
<div className="w-3/5">
<EmblaCarousel slides={slides} onSlideChange={handleSlideChange} />
</div>
<div className="w-2/5 flex flex-col justify-center items-start p-4 gap-6">
<AnimatePresence mode="wait" custom={direction}>
<motion.p
key={currentSlide}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.3 }}
className="inline-block text-text-active text-title1"
>
{slides[currentSlide].title}
</motion.p>
</AnimatePresence>
<div className="text-text flex flex-col md:flex-row gap-2 md:gap-7 text-small">
<p>Last edited 3 days ago </p>
<p> localhost: 3000</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 sm:gap-5 w-full">
<Button
size="default"
variant={'outline'}
className="gap-2 bg-bg-active border border-border-active w-full lg:w-auto"
>
<Pencil2Icon />
<p> Edit App </p>
</Button>
<Button size="default" variant={'ghost'} className="gap-2 w-full lg:w-auto">
<DotsVerticalIcon />
<p> Project settings</p>
</Button>
</div>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions app/src/routes/projects/SettingsTab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function SettingsTab() {
return <div className="flex h-[calc(100vh-5.5rem)] w-full">Settings</div>;
}
41 changes: 41 additions & 0 deletions app/src/routes/projects/TopBar/ModeToggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';

const ModeToggle = ({
currentTab,
setCurrentTab,
}: {
currentTab: 'projects' | 'settings';
setCurrentTab: (tab: 'projects' | 'settings') => void;
}) => {
return (
<ToggleGroup
type="single"
value={currentTab}
onValueChange={(value) => {
if (value) {
setCurrentTab(value as 'projects' | 'settings');
}
}}
className="mb-3 h-12"
>
<ToggleGroupItem
value="projects"
aria-label="Toggle Projects"
variant={'overline'}
className="flex items-end"
>
Projects
</ToggleGroupItem>
<ToggleGroupItem
value="settings"
aria-label="Toggle Settings"
variant={'overline'}
className="flex items-end"
>
Settings
</ToggleGroupItem>
</ToggleGroup>
);
};

export default ModeToggle;
Loading

0 comments on commit e690b41

Please sign in to comment.