├── src ├── constants.ts ├── modules │ ├── videos │ │ ├── constants.ts │ │ ├── ui │ │ │ └── components │ │ │ │ ├── video-player.tsx │ │ │ │ └── video-thumbnail.tsx │ │ └── server │ │ │ └── procedures.ts │ ├── categories │ │ └── server │ │ │ └── procedores.ts │ ├── studio │ │ ├── ui │ │ │ ├── views │ │ │ │ ├── video-view.tsx │ │ │ │ └── studio-view.tsx │ │ │ ├── layouts │ │ │ │ └── studio-layout.tsx │ │ │ ├── components │ │ │ │ ├── studio-navbar │ │ │ │ │ └── index.tsx │ │ │ │ ├── thumbnail-upload-modal.tsx │ │ │ │ ├── studio-sidebar │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── studio-sidebar-header.tsx │ │ │ │ ├── studio-upload-modal.tsx │ │ │ │ └── studio-uploader.tsx │ │ │ └── sections │ │ │ │ └── videos-section.tsx │ │ └── server │ │ │ └── procedures.ts │ ├── home │ │ └── ui │ │ │ ├── views │ │ │ └── home-view.tsx │ │ │ ├── components │ │ │ ├── home-sidebar │ │ │ │ ├── index.tsx │ │ │ │ ├── main-section.tsx │ │ │ │ └── personal-section.tsx │ │ │ └── home-navbar │ │ │ │ ├── search-input.tsx │ │ │ │ └── index.tsx │ │ │ ├── layouts │ │ │ └── home-layout.tsx │ │ │ └── sections │ │ │ └── categories-section.tsx │ └── auth │ │ └── ui │ │ └── components │ │ └── auth-button.tsx ├── app │ ├── favicon.ico │ ├── (auth) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ ├── sign-up │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (home) │ │ ├── layout.tsx │ │ ├── client.tsx │ │ └── page.tsx │ ├── (studio) │ │ ├── layout.tsx │ │ └── studio │ │ │ ├── page.tsx │ │ │ └── videos │ │ │ └── [videoId] │ │ │ └── page.tsx │ ├── api │ │ ├── uploadthing │ │ │ ├── route.ts │ │ │ └── core.ts │ │ ├── trpc │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── users │ │ │ └── webhook │ │ │ │ └── route.ts │ │ └── videos │ │ │ └── webhook │ │ │ └── route.ts │ ├── layout.tsx │ └── globals.css ├── db │ ├── index.ts │ └── schema.ts ├── lib │ ├── mux.ts │ ├── redis.ts │ ├── ratelimit.ts │ ├── uploadthing.ts │ └── utils.ts ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── toggle.tsx │ │ ├── badge.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── accordion.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── drawer.tsx │ │ ├── sheet.tsx │ │ ├── alert-dialog.tsx │ │ ├── command.tsx │ │ ├── navigation-menu.tsx │ │ ├── carousel.tsx │ │ ├── select.tsx │ │ ├── context-menu.tsx │ │ ├── menubar.tsx │ │ └── dropdown-menu.tsx │ ├── user-avatar.tsx │ ├── infinite-scroll.tsx │ ├── responsive-dialog.tsx │ └── filter-carousel.tsx ├── providers │ └── index.tsx ├── trpc │ ├── routers │ │ └── _app.ts │ ├── query-client.ts │ ├── server.tsx │ ├── init.ts │ └── client.tsx ├── hooks │ ├── use-mobile.tsx │ └── use-intersection-observer.ts ├── middleware.ts └── scripts │ └── seed-categories.ts ├── public ├── demo.jpeg ├── logo.svg ├── user-placeholder.svg └── placeholder.svg ├── postcss.config.mjs ├── .github └── workflows │ └── node.yml ├── drizzle.config.ts ├── eslint.config.mjs ├── .env.example ├── components.json ├── next.config.ts ├── .gitignore ├── tsconfig.json ├── scripts └── start-ngrok.mjs ├── system_prompts.ts ├── package.json ├── docs └── WEBHOOK_TROUBLESHOOTING.md └── README.md /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_LIMIT=5 -------------------------------------------------------------------------------- /src/modules/videos/constants.ts: -------------------------------------------------------------------------------- 1 | export const THUMBNAIL_FALLBACK = "/placeholder.svg"; 2 | -------------------------------------------------------------------------------- /public/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostovayne/Complete-Clone-of-Youtube/HEAD/public/demo.jpeg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostovayne/Complete-Clone-of-Youtube/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/neon-http"; 2 | 3 | export const db = drizzle(process.env.DATABASE_URL!); 4 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/mux.ts: -------------------------------------------------------------------------------- 1 | import Mux from "@mux/mux-node"; 2 | 3 | export const mux = new Mux({ 4 | tokenId: process.env.MUX_TOKEN_ID, 5 | tokenSecret: process.env.MUX_TOKEN_SECRET, 6 | }); 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis"; 2 | 3 | export const redis = new Redis({ 4 | url: process.env.UPSTASH_REDIS_REST_URL!, 5 | token: process.env.UPSTASH_REDIS_REST_TOKEN!, 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/lib/ratelimit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { redis } from "./redis"; 3 | 4 | export const ratelimit = new Ratelimit({ 5 | redis, 6 | limiter: Ratelimit.slidingWindow(10, "10s"), 7 | }); 8 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: "Node" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "Checkout" 13 | uses: actions/checkout@v4 14 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | interface AuthLayoutProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export default function AuthLayout({ children }: AuthLayoutProps) { 6 | return
{children}
; 7 | } 8 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HomeLayout } from "@/modules/home/ui/layouts/home-layout"; 2 | import { JSX } from "react"; 3 | 4 | interface LayoutProps { 5 | children: JSX.Element; 6 | } 7 | 8 | export default function Layout({ children }: LayoutProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateUploadButton, generateUploadDropzone } from "@uploadthing/react"; 2 | 3 | import type { OurFileRouter } from "@/app/api/uploadthing/core"; 4 | 5 | export const UploadButton = generateUploadButton(); 6 | export const UploadDropzone = generateUploadDropzone(); 7 | -------------------------------------------------------------------------------- /src/app/(studio)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { StudioLayout } from "@/modules/studio/ui/layouts/studio-layout"; 2 | import { JSX } from "react"; 3 | 4 | interface LayoutProps { 5 | children: JSX.Element; 6 | } 7 | 8 | export default function Layout({ children }: LayoutProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createRouteHandler({ 7 | router: ourFileRouter, 8 | 9 | // Apply an (optional) custom config: 10 | // config: { ... }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | dotenv.config({ 5 | path: ".env.local", 6 | }); 7 | 8 | export default defineConfig({ 9 | out: "./drizzle", 10 | schema: "./src/db/schema.ts", 11 | dialect: "postgresql", 12 | dbCredentials: { 13 | url: process.env.DATABASE_URL!, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /public/user-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/modules/categories/server/procedores.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db"; 2 | import { categories } from "@/db/schema"; 3 | import { baseProcedure, createTRPCRouter } from "@/trpc/init"; 4 | 5 | export const categoriesRouter = createTRPCRouter({ 6 | getMany: baseProcedure.query(async () => { 7 | const data = await db.select().from(categories); 8 | return data; 9 | }), 10 | }); 11 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | import { TRPCReactProvider } from "@/trpc/client"; 2 | import { HydrateClient } from "@/trpc/server"; 3 | 4 | export const TRPCProviderClient = ({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/modules/studio/ui/views/video-view.tsx: -------------------------------------------------------------------------------- 1 | import FormSection from "../sections/form-section"; 2 | 3 | interface VideoViewProps { 4 | videoId: string; 5 | } 6 | 7 | const VideoView = ({ videoId }: VideoViewProps) => { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default VideoView; 16 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; 2 | import { createTRPCContext } from '@/trpc/init'; 3 | import { appRouter } from '@/trpc/routers/_app'; 4 | const handler = (req: Request) => 5 | fetchRequestHandler({ 6 | endpoint: '/api/trpc', 7 | req, 8 | router: appRouter, 9 | createContext: createTRPCContext, 10 | }); 11 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /src/modules/home/ui/views/home-view.tsx: -------------------------------------------------------------------------------- 1 | import { CategoriesSection } from "../sections/categories-section"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | interface HomeViewProps { 6 | categoryId?: string; 7 | } 8 | 9 | export const HomeView = ({ categoryId }: HomeViewProps) => { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/modules/studio/ui/views/studio-view.tsx: -------------------------------------------------------------------------------- 1 | import { VideosSection } from "../sections/videos-section"; 2 | 3 | export const StudioView = () => { 4 | return ( 5 |
6 |
7 |

Channel Content

8 |

Manage your channel content and videos

9 |
10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= 3 | 4 | CLERK_SIGNING_SECRET="" 5 | 6 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 7 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 8 | 9 | NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/ 10 | NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/ 11 | 12 | DATABASE_URL="" 13 | 14 | 15 | UPSTASH_REDIS_REST_URL="" 16 | UPSTASH_REDIS_REST_TOKEN="" 17 | 18 | MUX_TOKEN_ID="" 19 | MUX_TOKEN_SECRET="" 20 | MUX_WEBHOOK_SECRET="" 21 | 22 | UPLOADTHING_TOKEN='' 23 | -------------------------------------------------------------------------------- /src/app/(home)/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTRPC } from "@/trpc/client"; 4 | import { useSuspenseQuery } from "@tanstack/react-query"; 5 | 6 | export const ClientPage = () => { 7 | const trpc = useTRPC(); 8 | const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions()); 9 | 10 | return ( 11 |
12 |

Esto es un client component, haciendo una llamada a una api con prefetching de la data

13 |
{JSON.stringify(data)}
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/trpc/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { categoriesRouter } from "@/modules/categories/server/procedores"; 2 | import { studioRouter } from "@/modules/studio/server/procedures"; 3 | 4 | import { videosRouter } from "@/modules/videos/server/procedures"; 5 | import { createTRPCRouter } from "../init"; 6 | 7 | export const appRouter = createTRPCRouter({ 8 | categories: categoriesRouter, 9 | studio: studioRouter, 10 | videos: videosRouter, 11 | }); 12 | // export type definition of API 13 | export type AppRouter = typeof appRouter; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/app/(studio)/studio/page.tsx: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LIMIT } from "@/constants"; 2 | import { StudioView } from "@/modules/studio/ui/views/studio-view"; 3 | import { HydrateClient, prefetch, trpc } from "@/trpc/server"; 4 | 5 | export const dynamic = "force-dynamic"; // Force dynamic rendering 6 | 7 | const Page = () => { 8 | prefetch(trpc.studio.getMany.infiniteQueryOptions({ limit: DEFAULT_LIMIT })); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Page; 18 | -------------------------------------------------------------------------------- /src/modules/home/ui/components/home-sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@/components/ui/separator"; 2 | import { Sidebar, SidebarContent } from "@/components/ui/sidebar"; 3 | import { MainSection } from "./main-section"; 4 | import { PersonalSection } from "./personal-section"; 5 | export const HomeSidebar = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "stream.mux.com", 10 | }, 11 | { 12 | protocol: "https", 13 | hostname: "tbw27c7h9z.ufs.sh", 14 | }, 15 | ], 16 | }, 17 | 18 | typescript: { 19 | ignoreBuildErrors: true, 20 | }, 21 | eslint: { 22 | ignoreDuringBuilds: true, 23 | }, 24 | 25 | experimental: { browserDebugInfoInTerminal: true }, 26 | }; 27 | 28 | export default nextConfig; 29 | -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const formatDuration = (duration: number) => { 9 | const seconds = Math.floor((duration % 60000) / 1000); 10 | const minutes = Math.floor(duration / 60000); 11 | 12 | return `${minutes.toString().padStart(2, "0")}: ${seconds.toString().padStart(2, "0")}`; 13 | }; 14 | 15 | 16 | 17 | export const snakeCaseToTitleCase = (str: string) => { 18 | return str.replace(/_/g, " ").replace(/\b\w/g, (match) => match.toUpperCase()); 19 | }; -------------------------------------------------------------------------------- /src/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query"; 2 | import superjson from "superjson"; 3 | 4 | export function makeQueryClient() { 5 | return new QueryClient({ 6 | defaultOptions: { 7 | queries: { 8 | staleTime: 30 * 1000, 9 | }, 10 | dehydrate: { 11 | serializeData: superjson.serialize, 12 | shouldDehydrateQuery: (query) => 13 | defaultShouldDehydrateQuery(query) || query.state.status === "pending", 14 | }, 15 | hydrate: { 16 | deserializeData: superjson.deserialize, 17 | 18 | }, 19 | }, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | .env.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /src/app/(studio)/studio/videos/[videoId]/page.tsx: -------------------------------------------------------------------------------- 1 | import VideoView from "@/modules/studio/ui/views/video-view"; 2 | import { HydrateClient, prefetch, trpc } from "@/trpc/server"; 3 | 4 | export const dynamic = "force-dynamic"; 5 | 6 | interface VideoPageProps { 7 | params: Promise<{ videoId: string }>; 8 | } 9 | 10 | const VideoPage = async ({ params }: VideoPageProps) => { 11 | const { videoId } = await params; 12 | 13 | prefetch(trpc.studio.getOne.queryOptions({ id: videoId })); 14 | prefetch(trpc.categories.getMany.queryOptions()) 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | export default VideoPage; 23 | -------------------------------------------------------------------------------- /src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { HomeView } from "@/modules/home/ui/views/home-view"; 2 | import { prefetch, trpc } from "@/trpc/server"; 3 | 4 | 5 | 6 | // export const dynamic = "force-dynamic"; 7 | 8 | interface PageProps { 9 | searchParams: Promise<{ 10 | categoryId?: string; 11 | }>; 12 | } 13 | 14 | const Page = async ({ searchParams }: PageProps) => { 15 | const { categoryId } = await searchParams; 16 | prefetch(trpc.categories.getMany.queryOptions()); // Toma el Prefetch de la funcion del server para recibir el QueryClient 17 | // const data = await caller.hello({ text: "Epsaind" }); => Llamada directa del server 18 | return ; 19 | }; 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/home/ui/layouts/home-layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarProvider } from "@/components/ui/sidebar"; 2 | import { JSX } from "react"; 3 | import { HomeNavbar } from "../components/home-navbar"; 4 | import { HomeSidebar } from "../components/home-sidebar"; 5 | 6 | interface HomeLayoutProps { 7 | children: JSX.Element; 8 | } 9 | 10 | export const HomeLayout = ({ children }: HomeLayoutProps) => { 11 | return ( 12 | 13 |
14 | 15 |
16 | 17 |
{children}
18 |
19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/studio/ui/layouts/studio-layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarProvider } from "@/components/ui/sidebar"; 2 | import { JSX } from "react"; 3 | import { StudioNavbar } from "../components/studio-navbar"; 4 | import { StudioSidebar } from "../components/studio-sidebar"; 5 | 6 | interface StudioLayout { 7 | children: JSX.Element; 8 | } 9 | 10 | export const StudioLayout = ({ children }: StudioLayout) => { 11 | return ( 12 | 13 |
14 | 15 |
16 | 17 |
{children}
18 |
19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |