Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable projects selection #370

Merged
merged 8 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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