Skip to content

Commit

Permalink
fea: complete yt-clone-fe
Browse files Browse the repository at this point in the history
  • Loading branch information
kunal232i committed Jun 9, 2024
1 parent 7b31a8d commit f4e92f1
Show file tree
Hide file tree
Showing 21 changed files with 5,686 additions and 196 deletions.
4,783 changes: 4,783 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"class-variance-authority": "^0.7.0",
"lucide-react": "^0.331.0",
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
Binary file removed public/youtube.png
Binary file not shown.
Binary file added public/yt_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 0 additions & 24 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
43 changes: 35 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import { AppBar } from '@/components/AppBar';
import {VGrid} from '@components/VGrid';
"use client";

import { useState } from "react";
import { CategoryPills } from "@components/CategoryPills";
import { categories, videos } from "../data/home";
import { PageHeader } from "../components/PageHeader";
import { VideoGridItem } from "../components/VideoGridItem";
import { Sidebar } from "../components/Sidebar";
import { SidebarProvider } from "../context/SidebarContext";

export default function App() {
const [selectedCategory, setSelectedCategory] = useState(categories[0]);

export default function Home() {
return (
<div>
<AppBar />
<VGrid />
</div>
)
<SidebarProvider>
<div className="max-h-screen flex flex-col">
<PageHeader />
<div className="grid grid-cols-[auto,1fr] flex-grow-1 overflow-auto">
<Sidebar />
<div className="overflow-x-hidden px-8 pb-4">
<div className="sticky top-0 bg-white z-10 pb-4">
<CategoryPills
categories={categories}
selectedCategory={selectedCategory}
onSelect={setSelectedCategory}
/>
</div>
<div className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
{videos.map((video) => (
<VideoGridItem key={video.id} {...video} />
))}
</div>
</div>
</div>
</div>
</SidebarProvider>
);
}
14 changes: 0 additions & 14 deletions src/components/AppBar.tsx

This file was deleted.

46 changes: 46 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { VariantProps, cva } from "class-variance-authority";
import { ComponentProps } from "react";
import { twMerge } from "tailwind-merge";

export const buttonStyles = cva(["transition-colors"], {
variants: {
variant: {
default: ["bg-secondary", "hover:bg-secondary-hover"],
ghost: ["hover:bg-gray-100"],
dark: [
"bg-secondary-dark",
"hover:bg-secondary-dark-hover",
"text-secondary",
],
},
size: {
default: [" rounded", "p-2"],
icon: [
"rounded-full",
"w-10",
"h-10",
"flex",
"items-center",
"justify-center",
"p-2.5",
],
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});

type ButtonProps = VariantProps<typeof buttonStyles> & ComponentProps<"button">;

export function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
{...props}
className={twMerge(buttonStyles({ variant, size }), className)}
/>
);
}
103 changes: 103 additions & 0 deletions src/components/CategoryPills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "./Button";
import { useEffect, useRef, useState } from "react";

type CategoryPillProps = {
categories: string[];
selectedCategory: string;
onSelect: (category: string) => void;
};

const TRANSLATE_AMOUNT = 200;

export function CategoryPills({
categories,
selectedCategory,
onSelect,
}: CategoryPillProps) {
const [translate, setTranslate] = useState(0);
const [isLeftVisible, setIsLeftVisible] = useState(false);
const [isRightVisible, setIsRightVisible] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (containerRef.current == null) return;

const observer = new ResizeObserver((entries) => {
const container = entries[0]?.target;
if (container == null) return;

setIsLeftVisible(translate > 0);
setIsRightVisible(
translate + container.clientWidth < container.scrollWidth
);
});

observer.observe(containerRef.current);

return () => {
observer.disconnect();
};
}, [categories, translate]);

return (
<div ref={containerRef} className="overflow-x-hidden relative">
<div
className="flex whitespace-nowrap gap-3 transition-transform w-[max-content]"
style={{ transform: `translateX(-${translate}px)` }}>
{categories.map((category) => (
<Button
key={category}
onClick={() => onSelect(category)}
variant={selectedCategory === category ? "dark" : "default"}
className="py-1 px-3 rounded-lg whitespace-nowrap">
{category}
</Button>
))}
</div>
{isLeftVisible && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 bg-gradient-to-r from-white from-50% to-transparent w-24 h-full">
<Button
variant="ghost"
size="icon"
className="h-full aspect-square w-auto p-1.5"
onClick={() => {
setTranslate((translate) => {
const newTranslate = translate - TRANSLATE_AMOUNT;
if (newTranslate <= 0) return 0;
return newTranslate;
});
}}>
<ChevronLeft />
</Button>
</div>
)}
{isRightVisible && (
<div className="absolute right-0 top-1/2 -translate-y-1/2 bg-gradient-to-l from-white from-50% to-transparent w-24 h-full flex justify-end">
<Button
variant="ghost"
size="icon"
className="h-full aspect-square w-auto p-1.5"
onClick={() => {
setTranslate((translate) => {
if (containerRef.current == null) {
return translate;
}
const newTranslate = translate + TRANSLATE_AMOUNT;
const edge = containerRef.current.scrollWidth;
const width = containerRef.current.clientWidth;
if (newTranslate + width >= edge) {
return edge - width;
}
return newTranslate;
});
}}>
<ChevronRight />
</Button>
</div>
)}
</div>
);
}
92 changes: 92 additions & 0 deletions src/components/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { ArrowLeft, Bell, Menu, Mic, Search, Upload, User } from "lucide-react";
import { Button } from "../components/Button";
import { useState } from "react";
import { useSidebarContext } from "../context/SidebarContext";

export function PageHeader() {
const [showFullWidthSearch, setShowFullWidthSearch] = useState(false);

return (
<div className="flex gap-10 lg:gap-20 justify-between pt-2 mb-6 mx-4">
<PageHeaderFirstSection hidden={showFullWidthSearch} />
<form
className={`gap-4 flex-grow justify-center ${
showFullWidthSearch ? "flex" : "hidden md:flex"
}`}>
{showFullWidthSearch && (
<Button
onClick={() => setShowFullWidthSearch(false)}
type="button"
size="icon"
variant="ghost"
className="flex-shrink-0">
<ArrowLeft />
</Button>
)}
<div className="flex flex-grow max-w-[600px]">
<input
type="search"
placeholder="Search"
className="rounded-l-full border border-secondary-border shadow-inner shadow-secondary py-1 px-4 text-lg w-full focus:border-blue-500 outline-none"
/>
<Button className="py-2 px-4 rounded-r-full border-secondary-border border border-l-0 flex-shrink-0">
<Search />
</Button>
</div>
<Button type="button" size="icon" className="flex-shrink-0">
<Mic />
</Button>
</form>
<div
className={`flex-shrink-0 md:gap-2 ${
showFullWidthSearch ? "hidden" : "flex"
}`}>
<Button
onClick={() => setShowFullWidthSearch(true)}
size="icon"
variant="ghost"
className="md:hidden">
<Search />
</Button>
<Button size="icon" variant="ghost" className="md:hidden">
<Mic />
</Button>
<Button size="icon" variant="ghost">
<Upload />
</Button>
<Button size="icon" variant="ghost">
<Bell />
</Button>
<Button size="icon" variant="ghost">
<User />
</Button>
</div>
</div>
);
}

type PageHeaderFirstSectionProps = {
hidden?: boolean;
};

export function PageHeaderFirstSection({
hidden = false,
}: PageHeaderFirstSectionProps) {
const { toggle } = useSidebarContext();

return (
<div
className={`gap-4 items-center flex-shrink-0 ${
hidden ? "hidden" : "flex"
}`}>
<Button onClick={toggle} variant="ghost" size="icon">
<Menu />
</Button>
<a href="/">
<img src="/yt_logo.png" className="h-6" />
</a>
</div>
);
}
30 changes: 0 additions & 30 deletions src/components/SearchBar.tsx

This file was deleted.

Loading

0 comments on commit f4e92f1

Please sign in to comment.