├── supabase ├── seed.sql ├── .gitignore └── config.toml ├── .eslintrc.json ├── app ├── favicon.ico ├── actions │ ├── getUserData.ts │ ├── getRatings.ts │ ├── getPlaylistData.ts │ ├── fetchThumbnails.ts │ ├── addRatings.ts │ └── addPlaylistData.ts ├── playlist │ ├── page.tsx │ ├── layout.tsx │ ├── rated-playlist │ │ └── page.tsx │ └── [playlistId] │ │ └── page.tsx ├── globals.css ├── layout.tsx ├── page.tsx ├── auth │ └── callback │ │ └── route.ts ├── login │ └── page.tsx └── signup │ └── page.tsx ├── public ├── name.png ├── rate.png ├── user.png ├── banner.png ├── custom.png ├── github.png ├── phone.png ├── rocket.png ├── table.png ├── watch.png ├── playlist.png ├── default-profile.jpg ├── default-thumbnail.png ├── category.svg ├── vercel.svg ├── play.svg ├── next.svg ├── not-found.svg ├── sieve.svg └── signup.svg ├── pr_info.json ├── postcss.config.mjs ├── utils ├── cn.ts ├── provider.tsx ├── getPlaylistCardData.ts ├── supabase │ ├── client.ts │ └── server.ts └── query.tsx ├── result.txt ├── lib └── utils.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── input.tsx │ ├── Marquee.tsx │ ├── avatar.tsx │ ├── aura-background.tsx │ ├── button.tsx │ ├── dialog.tsx │ └── select.tsx ├── loading.tsx ├── PlaylistCards.tsx ├── Playlist.tsx ├── typing-animation.tsx ├── Rating.tsx ├── PlaylistCard.tsx ├── Contribute.tsx ├── Card.tsx ├── Hero.tsx ├── Grid.tsx ├── Search.tsx ├── Categories.tsx ├── Filter.tsx ├── Footer.tsx ├── Wrapper.tsx ├── Rate.tsx ├── Navbar.tsx └── NavbarMobile.tsx ├── components.json ├── temp_playlist.json ├── .gitignore ├── next.config.mjs ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── add-playlist-video.md │ ├── bug_report.md │ └── add-playlist.yml ├── workflows │ ├── verify-playlist.yaml │ ├── process-contribution.yml │ └── add-playlist-workflow.yml └── scripts │ └── verify_playlist.py ├── types ├── Types.ts └── supabase.ts ├── package.json ├── middleware.ts ├── tailwind.config.ts ├── CONTRIBUTING.md ├── README.md └── playlist.json /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/name.png -------------------------------------------------------------------------------- /public/rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/rate.png -------------------------------------------------------------------------------- /public/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/user.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/custom.png -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/github.png -------------------------------------------------------------------------------- /public/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/phone.png -------------------------------------------------------------------------------- /public/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/rocket.png -------------------------------------------------------------------------------- /public/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/table.png -------------------------------------------------------------------------------- /public/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/watch.png -------------------------------------------------------------------------------- /public/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/playlist.png -------------------------------------------------------------------------------- /public/default-profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/default-profile.jpg -------------------------------------------------------------------------------- /public/default-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan28/sieve-repo/HEAD/public/default-thumbnail.png -------------------------------------------------------------------------------- /pr_info.json: -------------------------------------------------------------------------------- 1 | {"title": "Contribution from issue #46", "body": "This PR adds the contribution from issue #46", "head": "contribution-46", "base": "main"} -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /result.txt: -------------------------------------------------------------------------------- 1 | Playlist verification failed. Please address the following issues: 2 | 3 | - Summary too short for playlist Fastify for Lazy Developers: Quick and Scalable Backends. It should be at least 20 words long. 4 | 5 | Please review and update your submission. -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | 9 | export const disableNavAndFooter = [ 10 | "/login", 11 | "/signup" 12 | ] -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /utils/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { QueryClientProvider } from "@tanstack/react-query" 3 | import { getQueryClient } from "./query" 4 | 5 | export function Provider({children} : {children: React.ReactNode}) { 6 | const queryClient = getQueryClient(); 7 | 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/cn/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /utils/getPlaylistCardData.ts: -------------------------------------------------------------------------------- 1 | // utils/getPlaylistCardData.ts 2 | import getRatings from '@/app/actions/getRatings'; 3 | import getPlaylistData from '@/app/actions/getPlaylistData'; 4 | 5 | export async function getPlaylistCardData() { 6 | const ratings = await getRatings(); 7 | const playlistData = await getPlaylistData(); 8 | 9 | 10 | if (!ratings || !playlistData) return null; 11 | 12 | return { ratings, playlistData }; 13 | } -------------------------------------------------------------------------------- /utils/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { createBrowserClient } from '@supabase/ssr'; 3 | 4 | export function getSupabaseBrowserClient() { 5 | return createBrowserClient( 6 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 8 | ); 9 | } 10 | 11 | function useSupabaseClient() { 12 | return useMemo(getSupabaseBrowserClient, []); 13 | } 14 | 15 | export default useSupabaseClient; -------------------------------------------------------------------------------- /components/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /temp_playlist.json: -------------------------------------------------------------------------------- 1 | {"name": "Dark-Kernel", "playlist_link": "https://www.youtube.com/watch?v=oSZI11ilWjU&list=PL1H1sBF1VAKVmjZZr162aUNCt2Uy5ozAG", "summary": "Beginner-friendly tutorial on cybercrime investigation, whcih Provides practical examples for effective management and collection of data. by john hammond", "title": "Cybercrime Investigation", "category": "Cyber Security", "user_profile_link": "https://github.com/Dark-Kernel", "user_Image": "https://avatars.githubusercontent.com/Dark-Kernel"} -------------------------------------------------------------------------------- /public/category.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .playlist-cache 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | // logging:{ 4 | // fetches:{ 5 | // fullUrl: true, 6 | // } 7 | // }, 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: 'https', 12 | hostname: 'img.youtube.com', 13 | port: '', 14 | pathname: '/vi/**', 15 | }, 16 | { 17 | protocol: 'https', 18 | hostname: 'i.ytimg.com', 19 | port: '', 20 | pathname: '/vi/**', 21 | }, 22 | ], 23 | 24 | }, 25 | }; 26 | 27 | export default nextConfig; 28 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/actions/getUserData.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { User } from "@/types/Types"; 3 | import SupabaseServerClient from "@/utils/supabase/server"; 4 | 5 | 6 | const getUserData = async(): Promise => { 7 | const supabase = await SupabaseServerClient(); 8 | 9 | const{ data: {user}} = await supabase.auth.getUser(); 10 | 11 | if(!user){ 12 | console.log('NO USER', user); 13 | return null; 14 | } 15 | const {data, error} = await supabase.from('users').select('*').eq('id', user.id); 16 | 17 | if(error){ 18 | console.log(error, 'error'); 19 | return null 20 | } 21 | return data ? data[0] :null; 22 | } 23 | 24 | export default getUserData; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /utils/query.tsx: -------------------------------------------------------------------------------- 1 | import {QueryClient} from '@tanstack/react-query'; 2 | 3 | export function makeQueryClient() { 4 | return new QueryClient({ 5 | defaultOptions: { 6 | queries: { 7 | staleTime: 60*1000, 8 | } 9 | } 10 | }) 11 | } 12 | 13 | let browserQueryClient: QueryClient | undefined = undefined 14 | 15 | export function getQueryClient() { 16 | if(typeof window === 'undefined'){ 17 | 18 | // server: always make a new query client 19 | return makeQueryClient() 20 | }else { 21 | 22 | 23 | if(!browserQueryClient) browserQueryClient = makeQueryClient() 24 | return browserQueryClient 25 | } 26 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /components/PlaylistCards.tsx: -------------------------------------------------------------------------------- 1 | // components/PlaylistCards.tsx 2 | import React from 'react'; 3 | import PlaylistCard from './PlaylistCard'; 4 | import { PlaylistType } from "@/types/Types"; 5 | 6 | type EnrichedPlaylistType = PlaylistType & { 7 | playlistRating: number | null; 8 | avgPlaylistRate: string | null; 9 | }; 10 | 11 | type Props = { 12 | className: string; 13 | playlistData: EnrichedPlaylistType[]; 14 | }; 15 | 16 | const PlaylistCards = ({ className, playlistData }: Props) => { 17 | return ( 18 |
19 | {playlistData.map((playlist) => ( 20 | 21 | ))} 22 |
23 | ); 24 | }; 25 | 26 | export default PlaylistCards; -------------------------------------------------------------------------------- /app/actions/getRatings.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | import { RatingType } from "@/types/Types"; 3 | import SupabaseServerClient from "@/utils/supabase/server"; 4 | import { revalidatePath } from "next/cache"; 5 | 6 | const getRatings = async(): Promise => { 7 | const supabase = await SupabaseServerClient(); 8 | 9 | 10 | const{ data: {user}} = await supabase.auth.getUser(); 11 | 12 | // if(!user){ 13 | // console.log('NO USER', user); 14 | // return null; 15 | // } 16 | const {data, error} = await supabase.from('ratings').select().eq('user_id', user?.id); 17 | if(error){ 18 | console.log(error, 'error'); 19 | return null 20 | } 21 | revalidatePath('/') 22 | return data 23 | } 24 | 25 | export default getRatings; 26 | 27 | -------------------------------------------------------------------------------- /public/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /types/Types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | full_name: string; 4 | avatar_url: string; 5 | email: string; 6 | } 7 | export interface PlaylistType { 8 | avgPlaylistRate: string | null; 9 | id?: string, 10 | playlist_id?: number; 11 | user_name?: string; 12 | playlist_url?: string; 13 | playlist_summary?: string; 14 | playlist_title: string; 15 | playlist_category: string; 16 | playlist_image: string; 17 | user_profile_link: string; 18 | user_profile_image_link: string 19 | playlistRating: number | null, 20 | playlist_rates: number, 21 | inserted_at: string | undefined, 22 | views?: number, 23 | 24 | } 25 | 26 | export interface RatingType { 27 | id?: string, 28 | playlist_id: string, 29 | user_id: string, 30 | rating: any, 31 | 32 | } -------------------------------------------------------------------------------- /utils/supabase/server.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { createServerClient, type CookieOptions } from '@supabase/ssr'; 4 | import { cookies } from 'next/headers'; 5 | 6 | export default async function SupabaseServerClient() { 7 | const cookieStore = cookies(); 8 | 9 | return createServerClient( 10 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 12 | { 13 | cookies: { 14 | get(name: string) { 15 | return cookieStore.get(name)?.value; 16 | }, 17 | set(name: string, value: string, options: CookieOptions) { 18 | cookieStore.set({ name, value, ...options }); 19 | }, 20 | remove(name: string, options: CookieOptions) { 21 | cookieStore.set({ name, value: '', ...options }); 22 | }, 23 | }, 24 | } 25 | ); 26 | } -------------------------------------------------------------------------------- /app/playlist/page.tsx: -------------------------------------------------------------------------------- 1 | // app/playlists/page.tsx (Server Component) 2 | import getPlaylistData from "../actions/getPlaylistData"; 3 | import addOrUpdatePlaylistData from "../actions/addPlaylistData"; 4 | import Playlist from "@/components/Playlist"; 5 | import playlistJson from "@/playlist.json"; 6 | 7 | export default async function Page() { 8 | const playlistData = await getPlaylistData(); // ✅ Fetch from DB 9 | 10 | // ✅ Server-side comparison with JSON data 11 | console.log(playlistJson.length, 'playlistData') 12 | console.log(playlistData.totalCount! - playlistJson.length, 'remaning playlist') 13 | if (playlistData.totalCount !== playlistJson.length) { 14 | console.log("Updating playlist data on the server..."); 15 | await addOrUpdatePlaylistData(); // ✅ Update DB if mismatch 16 | } 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | :root { 5 | --foreground-rgb: 0, 0, 0; 6 | --background-start-rgb: 214, 219, 220; 7 | --background-end-rgb: 255, 255, 255; 8 | } 9 | body{ 10 | background:#12121b; 11 | 12 | } 13 | .hero-background { 14 | background: radial-gradient(circle, rgba(62,24,119,1) 0%, rgba(34,0,102,1) 100%); 15 | } 16 | 17 | .tagline{ 18 | 19 | font-family: var(--font-tenor_sans), sans-serif; 20 | 21 | } 22 | 23 | h1, h2, h3, h4, h5, h6 { 24 | font-family: var(--font-tenor_sans), sans-serif; 25 | } 26 | 27 | .no-scrollbar::-webkit-scrollbar { 28 | display: none; 29 | } 30 | 31 | /* Hide scrollbar for IE, Edge and Firefox */ 32 | .no-scrollbar { 33 | -ms-overflow-style: none; /* IE and Edge */ 34 | scrollbar-width: none; /* Firefox */ 35 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-playlist-video.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Add Playlist/Video 3 | about: Submit a new playlist or video to Sieve 4 | title: '' 5 | labels: good first issue 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🎬 Playlist Information 11 | 12 | ```json 13 | { 14 | "name": "Enter your name here", 15 | "playlist_link": "Enter the YouTube playlist or video link here", 16 | "summary": "Provide a summary here minimum 20 words", 17 | "title": "Enter the title here", 18 | "category": "Choose from the available categories below", 19 | "user_profile_link": "Enter your GitHub or other profile link here", 20 | "user_Image": "Link to your profile image" 21 | } 22 | ``` 23 | ## 📚 Playlist Categories 24 | ### Choose one category from the list below: 25 | ``` 26 | [ "Animation", "Backend Development", "Cyber Security", "Data Structures", "DevOps", "Frontend Development", "Full-Stack Development", "Machine Learning", "Projects", "UI/UX Design", "System Design"] 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/Playlist.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { useQuery } from "@tanstack/react-query"; 4 | import ClientSideSearchWrapper from "@/components/Wrapper"; 5 | import { PlaylistType } from "@/types/Types"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | 9 | interface Props { 10 | initialData: { 11 | data: PlaylistType[] | null; 12 | totalCount: number; 13 | }; 14 | } 15 | 16 | const Playlist = ({ initialData }: Props) => { 17 | const { 18 | data: playlistData, 19 | error: playlistError, 20 | isLoading, 21 | } = useQuery({ 22 | queryKey: ["playlists"], 23 | queryFn: async () => initialData.data || [], // ✅ Ensure array 24 | initialData: initialData.data || [], 25 | }); 26 | 27 | console.log(playlistError, "playlistData"); 28 | 29 | return ( 30 |
31 |
32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Playlist; 39 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tenor_Sans } from 'next/font/google' 2 | import { Libre_Franklin } from 'next/font/google' 3 | import './globals.css' 4 | import Navbar from '@/components/Navbar' 5 | import { Analytics } from "@vercel/analytics/react" 6 | import type { Metadata } from "next"; 7 | 8 | const tenor_sans = Tenor_Sans({ 9 | subsets: ['latin'], 10 | display: 'swap', 11 | variable: '--font-tenor_sans', 12 | weight: '400' 13 | }) 14 | const libre_franklin = Libre_Franklin({ 15 | subsets: ['latin'], 16 | display: 'swap', 17 | variable: '--font-libre_franklin', 18 | weight: '400' 19 | }) 20 | export const metadata: Metadata = { 21 | title: "Sieve is the IMDB For YouTube", 22 | description: "It is the ultimate place to find best playlist or videos", 23 | }; 24 | 25 | 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: Readonly<{ 30 | children: React.ReactNode; 31 | }>) { 32 | return ( 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /components/typing-animation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import { cn } from "@/utils/cn"; 6 | 7 | interface TypingAnimationProps { 8 | text: string; 9 | duration?: number; 10 | className?: string; 11 | } 12 | 13 | export default function TypingAnimation({ 14 | text, 15 | duration = 200, 16 | className, 17 | }: TypingAnimationProps) { 18 | const [displayedText, setDisplayedText] = useState(""); 19 | const [i, setI] = useState(0); 20 | 21 | useEffect(() => { 22 | const typingEffect = setInterval(() => { 23 | if (i < text.length) { 24 | setDisplayedText(text.substring(0, i + 1)); 25 | setI(i + 1); 26 | } else { 27 | clearInterval(typingEffect); 28 | } 29 | }, duration); 30 | 31 | return () => { 32 | clearInterval(typingEffect); 33 | }; 34 | }, [duration, i]); 35 | 36 | return ( 37 |

43 | {displayedText ? displayedText : text} 44 |

45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/playlist/layout.tsx: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic'; // Force dynamic rendering 2 | 3 | 4 | import { NavbarMobile } from '@/components/NavbarMobile' 5 | import { Provider } from '@/utils/provider'; 6 | import { getQueryClient } from '@/utils/query'; 7 | import getPlaylistData from '../actions/getPlaylistData'; 8 | import getRatings from '../actions/getRatings'; 9 | import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; 10 | 11 | 12 | 13 | 14 | export default async function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | 20 | const queryClient = getQueryClient() 21 | await queryClient.prefetchQuery({ 22 | queryKey: ["playlists"], 23 | queryFn: getPlaylistData, 24 | },) 25 | 26 | await queryClient.prefetchQuery({ 27 | queryKey: ["ratings"], 28 | queryFn: getRatings 29 | }) 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | {children} 39 | 40 | 41 | 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Head from 'next/head'; 3 | import Hero from '../components/Hero'; 4 | import { BentoGrids } from '@/components/Grid'; 5 | import Categories from '@/components/Categories'; 6 | import { Cards } from '@/components/Card'; 7 | import {Contrbute} from '@/components/Contribute' 8 | import Footer from '@/components/Footer'; 9 | import Navbar from '@/components/Navbar'; 10 | 11 | const Home = () => { 12 | return ( 13 |
14 | 15 | 16 | 21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 |
46 | ); 47 | }; 48 | 49 | export default Home; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/Marquee.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | 3 | interface MarqueeProps { 4 | className?: string; 5 | reverse?: boolean; 6 | pauseOnHover?: boolean; 7 | children?: React.ReactNode; 8 | vertical?: boolean; 9 | repeat?: number; 10 | [key: string]: any; 11 | } 12 | 13 | export default function Marquee({ 14 | className, 15 | reverse, 16 | pauseOnHover = false, 17 | children, 18 | vertical = false, 19 | repeat = 4, 20 | ...props 21 | }: MarqueeProps) { 22 | return ( 23 |
34 | {Array(repeat) 35 | .fill(0) 36 | .map((_, i) => ( 37 |
46 | {children} 47 |
48 | ))} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/actions/getPlaylistData.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { PlaylistType } from "@/types/Types"; 3 | import SupabaseServerClient from "@/utils/supabase/server"; 4 | 5 | const getPlaylistData = async (): Promise<{ data: PlaylistType[] | null; totalCount: number }> => { 6 | try { 7 | const supabase = await SupabaseServerClient(); 8 | 9 | // const { data: { user } } = await supabase.auth.getUser(); 10 | 11 | // if (!user) { 12 | // console.log('NO USER', user); 13 | // return { data: null, totalCount: 0 }; 14 | // } 15 | 16 | let queryBuilder = supabase 17 | .from('playlistsInfo') 18 | .select('*', { count: 'exact' }); 19 | 20 | // Add sorting 21 | queryBuilder = queryBuilder.order('inserted_at', { ascending: false }); 22 | 23 | const { data, error, count } = await queryBuilder; 24 | 25 | if (error) { 26 | console.log(error, 'error'); 27 | throw error; 28 | } 29 | 30 | if (!data || data.length === 0) { 31 | console.log("No Data", data); 32 | return { data: null, totalCount: 0 }; 33 | } 34 | 35 | return { data, totalCount: count ?? 0 }; 36 | } catch (error) { 37 | console.log(error, 'error'); 38 | throw error; 39 | } 40 | } 41 | 42 | export default getPlaylistData; -------------------------------------------------------------------------------- /components/Rating.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useState } from 'react'; 4 | import { Star } from 'lucide-react'; // Assuming you're using lucide-react for icons 5 | 6 | interface RatingProps { 7 | initialRating?: any ; 8 | onChange?: (rating: number) => void; 9 | readOnly?: boolean; 10 | className?: any 11 | } 12 | 13 | const Rating: React.FC = ({ initialRating = 0, onChange, readOnly = false, className }) => { 14 | const [rating, setRating] = useState(initialRating); 15 | const [hover, setHover] = useState(0); 16 | 17 | const handleRatingChange = (newRating: number) => { 18 | if (!readOnly) { 19 | setRating(newRating); 20 | if (onChange) { 21 | onChange(newRating); 22 | } 23 | } 24 | }; 25 | 26 | return ( 27 |
28 | {[1, 2, 3, 4, 5,6,7,8,9,10].map((star) => ( 29 | = star 33 | ? 'text-[#5899ed] fill-[#5899ed]' 34 | : 'text-gray-400' 35 | } ${readOnly ? 'cursor-default' : ''}`} 36 | onClick={() => handleRatingChange(star)} 37 | onMouseEnter={() => !readOnly && setHover(star)} 38 | onMouseLeave={() => !readOnly && setHover(0)} 39 | /> 40 | ))} 41 |
42 | ); 43 | }; 44 | 45 | export default Rating; -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers' 2 | import { NextResponse } from 'next/server' 3 | import { type CookieOptions, createServerClient } from '@supabase/ssr' 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams, origin } = new URL(request.url) 7 | const code = searchParams.get('code') 8 | // if "next" is in param, use it as the redirect URL 9 | const next = searchParams.get('next') ?? '/' 10 | 11 | if (code) { 12 | const cookieStore = cookies() 13 | const supabase = createServerClient( 14 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 15 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 16 | { 17 | cookies: { 18 | get(name: string) { 19 | return cookieStore.get(name)?.value 20 | }, 21 | set(name: string, value: string, options: CookieOptions) { 22 | cookieStore.set({ name, value, ...options }) 23 | }, 24 | remove(name: string, options: CookieOptions) { 25 | cookieStore.delete({ name, ...options }) 26 | }, 27 | }, 28 | } 29 | ) 30 | const { error } = await supabase.auth.exchangeCodeForSession(code) 31 | console.log(error); 32 | if (!error) { 33 | return NextResponse.redirect(`${origin}/playlist`) 34 | } 35 | } 36 | 37 | // return the user to an error page with instructions 38 | return NextResponse.redirect(`${origin}/auth/auth-code-error`) 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sieve", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "^1.0.4", 13 | "@radix-ui/react-dialog": "^1.1.1", 14 | "@radix-ui/react-select": "^2.1.1", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "@supabase/ssr": "^0.3.0", 17 | "@supabase/supabase-js": "^2.43.4", 18 | "@tabler/icons-react": "^3.4.0", 19 | "@tanstack/react-query": "^5.55.4", 20 | "@types/react-infinite-scroller": "^1.2.5", 21 | "@vercel/analytics": "^1.3.1", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.1.1", 24 | "framer-motion": "^11.2.10", 25 | "googleapis": "^140.0.1", 26 | "lucide-react": "^0.378.0", 27 | "next": "14.2.3", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-infinite-scroller": "^1.2.6", 31 | "react-query": "^3.39.3", 32 | "simplex-noise": "^4.0.1", 33 | "tailwind-merge": "^2.3.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "use-debounce": "^10.0.1" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20", 39 | "@types/react": "^18", 40 | "@types/react-dom": "^18", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.2.3", 43 | "postcss": "^8", 44 | "supabase": "^1.172.2", 45 | "tailwindcss": "^3.4.1", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/actions/fetchThumbnails.ts: -------------------------------------------------------------------------------- 1 | import { google } from 'googleapis'; 2 | 3 | const youtube = google.youtube({ 4 | version: 'v3', 5 | auth: process.env.YOUTUBE_API_KEY // Store your API key in environment variables 6 | }); 7 | 8 | export const getThumbnailUrl = async (url: string): Promise => { 9 | const videoIdMatch = url.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/); 10 | const playlistIdMatch = url.match(/list=([^&]+)/); 11 | 12 | try { 13 | let thumbnailUrl = '/default-thumbnail.png'; 14 | 15 | if (videoIdMatch) { 16 | const videoId = videoIdMatch[1]; 17 | const response = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; 18 | if (response) { 19 | thumbnailUrl = response; 20 | } else { 21 | console.log('No items found for video ID:', videoId); 22 | } 23 | }else if (playlistIdMatch) { 24 | const playlistId = playlistIdMatch[1]; 25 | const response = await youtube.playlists.list({ 26 | part: ['snippet'], 27 | id: [playlistId] 28 | }); 29 | 30 | if ((response?.data?.items?.length ?? 0) > 0) { 31 | thumbnailUrl = response.data.items?.[0]?.snippet?.thumbnails?.high?.url ?? thumbnailUrl; 32 | } 33 | } 34 | 35 | return thumbnailUrl; 36 | } catch (error) { 37 | console.error('Error fetching thumbnail:', error); 38 | return '/default-thumbnail.png'; 39 | } 40 | }; -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/utils/cn" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /app/actions/addRatings.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { RatingType } from '@/types/Types' 3 | import SupabaseServerClient from '@/utils/supabase/server' 4 | import { revalidatePath } from 'next/cache'; 5 | import { redirect } from 'next/navigation'; 6 | 7 | 8 | 9 | const addRatings = async (ratingType: RatingType): Promise => { 10 | const supabase = await SupabaseServerClient(); 11 | 12 | const{ data: {user}} = await supabase.auth.getUser(); 13 | if(!user){ 14 | console.log('NO USER', user); 15 | return null; 16 | } 17 | 18 | const {data: alreadyRatedPlaylist, error: fetchError} = await supabase.from('ratings').select("*") 19 | 20 | if(!alreadyRatedPlaylist) return null 21 | 22 | if (fetchError) { 23 | console.log(fetchError, 'fetchError'); 24 | return null; 25 | } 26 | const existingRatingPlaylist = alreadyRatedPlaylist.map(rated => rated.playlist_id); 27 | 28 | if(existingRatingPlaylist.includes(ratingType.playlist_id)){ 29 | const {data, error} = await supabase.from('ratings').update({ 30 | rating: ratingType.rating, 31 | playlist_id: ratingType.playlist_id, 32 | user_id: ratingType.user_id 33 | }).eq('playlist_id', ratingType.playlist_id).select(); 34 | revalidatePath('/playlist') 35 | 36 | if(error){ 37 | console.log(error, 'updateError'); 38 | return null 39 | } 40 | if(!data) return null 41 | 42 | } 43 | 44 | const {data, error} = await supabase.from('ratings').insert({ 45 | rating: ratingType.rating, 46 | playlist_id: ratingType.playlist_id, 47 | user_id: ratingType.user_id 48 | }).select() 49 | 50 | 51 | 52 | if (error) { 53 | console.error('Error inserting rating:', error); 54 | } else { 55 | // console.log('Rating inserted successfully:', data); 56 | } 57 | 58 | revalidatePath('/playlist') 59 | 60 | 61 | return data ? data[0]: null 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | } 70 | 71 | export default addRatings -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-playlist.yml: -------------------------------------------------------------------------------- 1 | name: Add YouTube Playlist 2 | description: Submit an educational YouTube playlist 3 | title: "[Playlist]: " 4 | labels: ["playlist-submission"] 5 | body: 6 | - type: input 7 | id: title 8 | attributes: 9 | label: Playlist Title 10 | description: The full title of the YouTube playlist 11 | placeholder: "e.g., < Name > by < Creator >" 12 | validations: 13 | required: true 14 | 15 | - type: input 16 | id: playlist_link 17 | attributes: 18 | label: YouTube Playlist URL 19 | description: Full link to the YouTube playlist 20 | placeholder: "https://www.youtube.com/playlist?list=..." 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: summary 26 | attributes: 27 | label: Playlist Summary 28 | description: Provide a detailed description of what this playlist teaches 29 | placeholder: "Describe the content, teaching style, and what learners can expect from this playlist..." 30 | validations: 31 | required: true 32 | 33 | - type: dropdown 34 | id: category 35 | attributes: 36 | label: Category 37 | description: Select the main category for this educational playlist 38 | options: 39 | - Frontend Development 40 | - Backend Development 41 | - Full Stack Development 42 | - DevOps 43 | - Mobile Development 44 | - Data Structures & Algorithms 45 | - System Design 46 | - Cloud Computing 47 | - Machine Learning/AI 48 | - Blockchain 49 | - Other 50 | validations: 51 | required: true 52 | 53 | # - type: input 54 | # id: name 55 | # attributes: 56 | # label: Your GitHub Username 57 | # description: Enter your GitHub username (without @) 58 | # placeholder: "e.g., CODEX108" 59 | # validations: 60 | # required: true 61 | -------------------------------------------------------------------------------- /.github/workflows/verify-playlist.yaml: -------------------------------------------------------------------------------- 1 | name: Verify Playlist Submission 2 | 3 | on: 4 | pull_request_target: 5 | paths: 6 | - 'playlist.json' 7 | 8 | permissions: 9 | contents: read 10 | issues: write 11 | pull-requests: write 12 | 13 | jobs: 14 | verify: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.sha }} 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.x' 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install jsonschema requests 31 | 32 | - name: Run verification script 33 | id: verify 34 | run: python .github/scripts/verify_playlist.py 35 | 36 | - name: Read verification result 37 | id: read-result 38 | run: | 39 | if [ -f result.txt ]; then 40 | RESULT=$(cat result.txt) 41 | echo "RESULT<> $GITHUB_ENV 42 | echo "$RESULT" >> $GITHUB_ENV 43 | echo "EOF" >> $GITHUB_ENV 44 | else 45 | echo "RESULT=Verification script did not produce a result file." >> $GITHUB_ENV 46 | fi 47 | 48 | - name: Comment on PR 49 | uses: actions/github-script@v6 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | script: | 53 | const output = process.env.RESULT || "❌ Playlist verification failed."; 54 | await github.rest.issues.createComment({ 55 | ...context.repo, 56 | issue_number: context.issue.number, 57 | body: output 58 | }); 59 | 60 | - name: Set workflow status 61 | if: ${{ !startsWith(env.RESULT, '✅ Playlist verification passed!') }} 62 | run: exit 1 -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from '@supabase/ssr'; 2 | import { NextResponse, type NextRequest } from 'next/server'; 3 | 4 | export async function middleware(request: NextRequest) { 5 | let response = NextResponse.next({ 6 | request: { 7 | headers: request.headers, 8 | }, 9 | }); 10 | 11 | const supabase = createServerClient( 12 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 13 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 14 | { 15 | cookies: { 16 | get(name: string) { 17 | return request.cookies.get(name)?.value; 18 | }, 19 | set(name: string, value: string, options: CookieOptions) { 20 | request.cookies.set({ 21 | name, 22 | value, 23 | ...options, 24 | }); 25 | response = NextResponse.next({ 26 | request: { 27 | headers: request.headers, 28 | }, 29 | }); 30 | response.cookies.set({ 31 | name, 32 | value, 33 | ...options, 34 | }); 35 | }, 36 | remove(name: string, options: CookieOptions) { 37 | request.cookies.set({ 38 | name, 39 | value: '', 40 | ...options, 41 | }); 42 | response = NextResponse.next({ 43 | request: { 44 | headers: request.headers, 45 | }, 46 | }); 47 | response.cookies.set({ 48 | name, 49 | value: '', 50 | ...options, 51 | }); 52 | }, 53 | }, 54 | } 55 | ); 56 | 57 | await supabase.auth.getUser(); 58 | 59 | return response; 60 | } 61 | 62 | export const config = { 63 | matcher: [ 64 | /* 65 | * Match all request paths except for the ones starting with: 66 | * - _next/static (static files) 67 | * - _next/image (image optimization files) 68 | * - favicon.ico (favicon file) 69 | * Feel free to modify this pattern to include more paths. 70 | */ 71 | '/((?!_next/static|_next/image|favicon.ico).*)', 72 | ], 73 | }; -------------------------------------------------------------------------------- /components/PlaylistCard.tsx: -------------------------------------------------------------------------------- 1 | // components/PlaylistCard.tsx 2 | import React, { Suspense } from 'react'; 3 | import Image from 'next/image'; 4 | import { Star, Eye } from 'lucide-react'; 5 | import Link from 'next/link'; 6 | import Rate from './Rate'; 7 | import { PlaylistType } from '@/types/Types'; 8 | import Loading from './loading'; 9 | 10 | type PlaylistCardProps = PlaylistType & { 11 | playlistRating: number | null; 12 | avgPlaylistRate: string | null; 13 | }; 14 | 15 | const PlaylistCard = (props: PlaylistCardProps) => { 16 | return ( 17 | }> 18 |
19 | 20 | {props.playlist_title} 26 |
27 | {props.playlist_category} 28 |
29 | 30 |
31 |
{props.playlist_title}
32 |
33 | 34 | 35 | {props.avgPlaylistRate || '0.0'} 36 | 37 | {/* 38 | mb-2 39 | {props.views || '0'}K 40 | */} 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default PlaylistCard; -------------------------------------------------------------------------------- /components/Contribute.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import React from "react"; 5 | import { AuroraBackground } from "./ui/aura-background"; 6 | import Link from "next/link"; 7 | import {ArrowUpRight} from 'lucide-react' 8 | 9 | 10 | export function Contrbute() { 11 | return ( 12 | 13 | 23 |
24 | Join the community and contribute your own learning 25 |
26 |
27 | Click here to follow the steps to share your playlists with fellow 28 | students to help them find the best resources 29 |
30 | 31 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | 4 | const { 5 | default: flattenColorPalette, 6 | } = require("tailwindcss/lib/util/flattenColorPalette"); 7 | 8 | const config = { 9 | darkMode: ["class"], 10 | content: [ 11 | './pages/**/*.{ts,tsx}', 12 | './components/**/*.{ts,tsx}', 13 | './app/**/*.{ts,tsx}', 14 | './src/**/*.{ts,tsx}', 15 | ], 16 | prefix: "", 17 | theme: { 18 | container: { 19 | center: true, 20 | padding: "2rem", 21 | screens: { 22 | "2xl": "1400px", 23 | }, 24 | }, 25 | extend: { 26 | keyframes: { 27 | "accordion-down": { 28 | from: { height: "0" }, 29 | to: { height: "var(--radix-accordion-content-height)" }, 30 | }, 31 | aurora: { 32 | from: { 33 | backgroundPosition: "50% 50%, 50% 50%", 34 | }, 35 | to: { 36 | backgroundPosition: "350% 50%, 350% 50%", 37 | }, 38 | }, 39 | "accordion-up": { 40 | from: { height: "var(--radix-accordion-content-height)" }, 41 | to: { height: "0" }, 42 | }, 43 | marquee: { 44 | from: { transform: "translateX(0)" }, 45 | to: { transform: "translateX(calc(-100% - var(--gap)))" }, 46 | }, 47 | "marquee-vertical": { 48 | from: { transform: "translateY(0)" }, 49 | to: { transform: "translateY(calc(-100% - var(--gap)))" }, 50 | }, 51 | 52 | }, 53 | animation: { 54 | "accordion-down": "accordion-down 0.2s ease-out", 55 | "accordion-up": "accordion-up 0.2s ease-out", 56 | aurora: "aurora 60s linear infinite", 57 | marquee: "marquee var(--duration) linear infinite", 58 | "marquee-vertical": "marquee-vertical var(--duration) linear infinite", 59 | 60 | }, 61 | }, 62 | }, 63 | plugins: [require("tailwindcss-animate"),addVariablesForColors ], 64 | } satisfies Config 65 | 66 | function addVariablesForColors({ addBase, theme }: any) { 67 | let allColors = flattenColorPalette(theme("colors")); 68 | let newVars = Object.fromEntries( 69 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]) 70 | ); 71 | 72 | addBase({ 73 | ":root": newVars, 74 | }); 75 | } 76 | export default config -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Code 🤝 2 | 3 | We welcome contributions from the community! To contribute: 4 | 5 | Please check the available [Issues](https://github.com/anaskhan28/sieve-repo/issues) for the website first then statrt contributing! 6 | 7 | 1. **Fork the repository**: 8 | 9 | - Click the "Fork" button on the top right corner of this repository. 10 | 2. **Clone your fork**: 11 | 12 | ```bash 13 | git clone https://github.com/anaskhan28/sieve-repo.git 14 | cd sieve-repo 15 | ``` 16 | 3. **Create a new branch**: 17 | 18 | ```bash 19 | git checkout -b feature/your-feature-name 20 | ``` 21 | 4. **Make your changes** and **commit**: 22 | 23 | ```bash 24 | git commit -m "Add your commit message" 25 | ``` 26 | 5. **Push to the branch**: 27 | 28 | ```bash 29 | git push origin feature/your-feature-name 30 | ``` 31 | 6. **Open a Pull Request**: 32 | 33 | - Navigate to the original repository and click the "New Pull Request" button. 34 | 35 | ### Contributing Playlists/Video 🫡 36 | 37 | To add a new playlist, please follow these steps: 38 | 39 | 1. Open a [New Issue](https://github.com/anaskhan28/sieve-repo/issues/new/choose) on Issues tab. 40 | 2. Click on **`Add Playlist/Video`** 41 | 3. Add your playlist/video details in the following format: 42 | 43 | ```json 44 | { 45 | "name": "Anas Khan", 46 | "playlist_link": "https://www.youtube.com/playlist?list=PLIY8eNdw5tW_7-QrsY_n9nC0Xfhs1tLEK", 47 | "summary": "All the concepts, algorithms and protocols related to Network Security which you as an IT student will need the most.", 48 | "title": "Network Security", 49 | "category": "Cyber Security", 50 | "user_profile_link": "https://github.com/anaskhan28", 51 | "user_Image": "https://avatars.githubusercontent.com/u/87796038?s=96&v=4" 52 | } 53 | 54 | ``` 55 | 4. Choose one category from the list below: 56 | 57 | ``` 58 | [ 59 | "Animation", 60 | "Backend Development", 61 | "Cyber Security", 62 | "Data Structures", 63 | "Devops", 64 | "Frontend Development", 65 | "Full-Stack Development", 66 | "Machine Learning", 67 | "Projects", 68 | "UI UX Design", 69 | "System Design", 70 | ] 71 | 72 | ``` 73 | 5. Open the Issue and if all the verification passed your playlist or video will be added in Sieve. 74 | -------------------------------------------------------------------------------- /components/ui/aura-background.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/utils/cn"; 3 | import React, { ReactNode } from "react"; 4 | 5 | interface AuroraBackgroundProps extends React.HTMLProps { 6 | children: ReactNode; 7 | showRadialGradient?: boolean; 8 | } 9 | 10 | export const AuroraBackground = ({ 11 | className, 12 | children, 13 | showRadialGradient = true, 14 | ...props 15 | }: AuroraBackgroundProps) => { 16 | return ( 17 |
18 |
25 |
26 |
48 |
49 | {children} 50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /components/Card.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | 5 | const Card = ( 6 | { 7 | image, 8 | title, 9 | paragraph, 10 | }: { 11 | image: string; 12 | title: string; 13 | paragraph: string; 14 | } 15 | ) => { 16 | return ( 17 | 22 | 23 | {title} 30 |

{title}

31 |

32 | {paragraph} 33 |

34 | 35 | ); 36 | }; 37 | 38 | export const Cards = () => { 39 | return ( 40 |
41 |
42 |
43 |

So, how does it work?

44 |

45 | The process is pretty simple and yet amazing. 46 |

47 |
48 |
49 | 54 | 59 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90", 13 | destructive: 14 | "bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90", 15 | outline: 16 | "border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50", 17 | secondary: 18 | "bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80", 19 | ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50", 20 | link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/Hero.tsx: -------------------------------------------------------------------------------- 1 | // components/Hero.tsx 2 | import React from 'react'; 3 | import Image from 'next/image'; 4 | import Background from '@/public/logos.svg' 5 | import Link from 'next/link'; 6 | import {ArrowUpRight} from 'lucide-react' 7 | import getUserData from '@/app/actions/getUserData'; 8 | import TypingAnimation from './typing-animation'; 9 | 10 | const Hero: React.FC = async() => { 11 | const userData = await getUserData(); 12 | 13 | 14 | 15 | return ( 16 |
17 |
18 |
19 | Welcome to the Sieve 20 | 25 | 26 |

27 | Your Gateway to the Best Tech Learning Playlists. Sieve is the IMDB for YouTube learning, helping you find the best content effortlessly 28 |

29 | 46 | 47 |
48 |
49 | 50 |
51 | ); 52 | }; 53 | 54 | export default Hero; 55 | -------------------------------------------------------------------------------- /app/playlist/rated-playlist/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useMemo } from 'react' 4 | import PlaylistCards from '@/components/PlaylistCards'; 5 | import getRatings from '../../actions/getRatings'; 6 | import { PlaylistType } from '@/types/Types'; // Make sure to import your PlaylistType 7 | import Image from 'next/image'; 8 | import { useQuery } from '@tanstack/react-query'; 9 | import getPlaylistData from '@/app/actions/getPlaylistData'; 10 | export const dynamic = 'force-dynamic' 11 | type EnrichedPlaylistType = PlaylistType & { 12 | playlistRating: number | null; 13 | avgPlaylistRate: string | null; 14 | }; 15 | 16 | const RatedPlaylist = () => { 17 | 18 | 19 | const { data: ratingData, error: ratingError } = useQuery({ 20 | queryKey: ["ratings"], 21 | queryFn: getRatings, 22 | }); 23 | 24 | const { 25 | data: playlistData, 26 | error: playlistError, 27 | isLoading 28 | } = useQuery({ 29 | queryKey: ["playlists"], 30 | queryFn: getPlaylistData, 31 | }); 32 | 33 | 34 | console.log(playlistData, 'playlsitData') 35 | 36 | 37 | const ratedPlaylistDetails = useMemo(() => { 38 | if(!playlistData || !playlistData.data || !ratingData || !ratingData) return []; 39 | 40 | return playlistData.data 41 | .filter((playlist) => ratingData.some((rating) => rating.playlist_id === playlist.id)) 42 | .map((playlist) => { 43 | const userRating = ratingData.find((rating) => rating.playlist_id === playlist.id); 44 | return { 45 | ...playlist, 46 | playlistRating: userRating ? userRating.rating : null, 47 | avgPlaylistRate: playlist.playlist_rates?.toFixed(1) || null 48 | }; 49 | }); 50 | }, [playlistData, ratingData]) 51 | 52 | // if (isLoading) return
Loading...
; 53 | // if (playlistError || ratingError) return
Error loading data
; 54 | 55 | 56 | return ( 57 |
58 | { 59 | ratedPlaylistDetails.length>0 ? 60 |
61 | 62 |
63 | : 64 | ( 65 |
66 | not-found 67 |
68 | ) 69 | } 70 | 71 |
72 | ) 73 | } 74 | 75 | export default RatedPlaylist -------------------------------------------------------------------------------- /public/not-found.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Image from 'next/image'; 3 | import React from 'react' 4 | import Link from 'next/link'; 5 | import useSupabaseClient from '@/utils/supabase/client'; 6 | import { Provider } from '@supabase/supabase-js'; 7 | 8 | type Props = {} 9 | 10 | const Login = (props: Props) => { 11 | 12 | 13 | const supabase = useSupabaseClient(); 14 | 15 | 16 | const socialAuth = async (provider: Provider) =>{ 17 | 18 | 19 | await supabase.auth.signInWithOAuth({ 20 | provider, 21 | options: { 22 | redirectTo: `${location.origin}/auth/callback`, 23 | } 24 | }) 25 | } 26 | return ( 27 |
28 |
29 | sieve 30 | 31 |
32 | sieve 33 | 34 |
35 |
36 | 37 |
38 |
39 | Welcome back to Sieve 40 |

Continue a journey with us

41 |
42 |
43 | 50 | 57 | New One? Signup Now 59 |
60 | 61 |
62 | 63 | 64 | 65 | 66 |
67 | ) 68 | } 69 | 70 | export default Login; -------------------------------------------------------------------------------- /components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | import React from "react"; 3 | import { 4 | IconClipboardCopy, 5 | IconFileBroken, 6 | IconSignature, 7 | IconTableColumn, 8 | } from "@tabler/icons-react"; 9 | import Image from "next/image"; 10 | 11 | export function BentoGrids() { 12 | return ( 13 |
14 |
15 | Overwhelmed by the sheer volume of tech content on YouTube? 16 |
17 | 18 |
19 | 20 |
21 |
22 | rocket 23 |
24 | Save Time, and Learn Better 25 | 26 | 27 |
28 |
29 |
30 | Awesome 31 | rocket 32 |
Trusted resources to learn anything
33 |
34 |
35 |
Discover the Best Playlists
36 | rocket 37 | 38 |
39 |
40 |
41 |
42 |
With Sieve
44 | Curate Your Learning Path
45 |
Goal
46 |
47 |
48 | 49 | 50 |
51 | 52 | 53 | 54 |
55 |
56 | ); 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /components/Search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Input } from "@/components/ui/input" 4 | import { Button } from "@/components/ui/button" 5 | import { FormEvent } from "react" 6 | import { Search as SearchIcon } from 'lucide-react'; 7 | import { useRouter, useSearchParams } from 'next/navigation'; 8 | 9 | import { 10 | Select, 11 | SelectContent, 12 | SelectGroup, 13 | SelectItem, 14 | SelectLabel, 15 | SelectTrigger, 16 | SelectValue, 17 | } from "@/components/ui/select" 18 | type SearchProps = { 19 | 20 | 21 | } 22 | 23 | type SortOption = 'rating' | 'newest' | 'oldest'; 24 | 25 | 26 | export default function Search({ }: SearchProps) { 27 | const router = useRouter() 28 | 29 | const searchParams = useSearchParams(); 30 | 31 | const setSearch = (search: string) => { 32 | const params = new URLSearchParams(searchParams); 33 | if (search) { 34 | params.set('search', search); 35 | } else { 36 | params.delete('search'); 37 | } 38 | router.push(`/playlist?${params.toString()}`); 39 | }; 40 | 41 | const setSort = (sort: SortOption) => { 42 | const params = new URLSearchParams(searchParams); 43 | if (sort) { 44 | params.set('sort', sort); 45 | } else { 46 | params.delete('sort'); 47 | } 48 | router.push(`/playlist?${params.toString()}`); 49 | }; 50 | 51 | const handleSubmit = (e: FormEvent) => { 52 | e.preventDefault(); 53 | const formData = new FormData(e.currentTarget); 54 | const query = formData.get('query') as string; 55 | setSearch(query); 56 | }; 57 | 58 | 59 | 60 | return ( 61 |
62 |
63 | 71 | 72 | 73 | 74 |
75 | 92 |
93 | ) 94 | } -------------------------------------------------------------------------------- /components/Categories.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | import Marquee from "./ui/Marquee"; 3 | import { randomUUID } from "crypto"; 4 | const category = [ 5 | { 6 | name: 'Frontend', 7 | circleColour: 'D8A953', 8 | background: 'F6F3F0', 9 | textColor: 'D8A953', 10 | 11 | }, 12 | { 13 | name: 'Backend', 14 | circleColour: 'AB7FE6', 15 | background: 'F3F1FA', 16 | textColor: 'AB7FE6', 17 | 18 | }, 19 | { 20 | name: 'Android', 21 | circleColour: '57CBD0', 22 | background: 'EEF6F8', 23 | textColor: '57CBD0', 24 | 25 | }, 26 | { 27 | name: 'Data Structures', 28 | circleColour: '7F8FE9', 29 | background: 'F1F2FA', 30 | textColor: '7F8FE9', 31 | 32 | }, 33 | { 34 | name: 'Machine Learning', 35 | circleColour: '6BAC65', 36 | background: 'ECF2EE', 37 | textColor: '6BAC65', 38 | 39 | }, 40 | { 41 | name: 'Open Source', 42 | circleColour: 'F0793A', 43 | background: 'F9F1EE', 44 | textColor: 'F0793A', 45 | 46 | }, 47 | 48 | ]; 49 | 50 | const firstRow = category.slice(0, category.length / 2); 51 | const secondRow = category.slice(category.length / 2); 52 | 53 | const CategoryCard = ({ 54 | 55 | circleColour, 56 | name, 57 | background, 58 | textColor, 59 | 60 | }: { 61 | circleColour: string; 62 | name: string; 63 | background:string; 64 | textColor: string; 65 | 66 | 67 | 68 | }) => { 69 | return ( 70 |
74 |
75 |
77 |
79 |
80 |
{name}
82 |
83 |
84 | ); 85 | }; 86 | 87 | const Categories = () => { 88 | return ( 89 |
90 | 91 | { 92 | firstRow.map((category, i) =>( 93 | 94 | ) ) 95 | } 96 | 97 | 98 | 99 | { 100 | secondRow.map((category, i) =>( 101 | 102 | ) ) 103 | } 104 | 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | 112 | 113 | 114 | export default Categories; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sieve 🎬📚 2 | 3 | Welcome to **Sieve**, an IMDB-like platform for discovering and rating the best YouTube playlists or videos on software development, web development, programming languages, frameworks, design, open source, and more. This platform aims to help newcomers and enthusiasts find high-quality and most-rated tutorials. 4 | 5 | ![Sieve Banner](public/banner.png) 6 | 7 | ## Table of Contents 📑 8 | 9 | - [Project Overview](#project-overview-📝) 10 | - [Features](#features-✨) 11 | - [Tech Stack](#tech-stack-🛠️) 12 | - [Installation](#installation-📥) 13 | - [Usage](#usage-🚀) 14 | - [Contributing](CONTRIBUTING.md) 15 | - [License](#license-📄) 16 | - [Contact](#contact-📧) 17 | 18 | ## Project Overview 📝 19 | 20 | Sieve is designed to be a comprehensive and user-friendly platform where users can: 21 | 22 | - Discover curated YouTube playlists on various technical topics. 23 | - Rate and review playlists to provide feedback and help others find valuable content. 24 | - Contribute by adding new playlists and enhancing the platform’s content library. 25 | 26 | ## Features ✨ 27 | 28 | - **User Registration and Authentication**: Secure user sign-up and login functionalities. 29 | - **Playlist Discovery**: Browse and search for the best playlists on different technical subjects. 30 | - **Rating System**: Rate and review playlists to help others gauge the quality of the content. 31 | - **User Contributions**: Submit new playlists to the platform, enriching the content repository. 32 | 33 | ## Tech Stack 🛠️ 34 | 35 | - **Frontend**: Next.js, TypeScript 36 | - **Backend**: Supabase, Next.js Server Actions 37 | - **Database**: Postgres 38 | - **Validation**: Zod 39 | 40 | ## Installation 📥 41 | 42 | To get a local copy up and running, follow these simple steps: 43 | 44 | 1. **Clone the repository**: 45 | 46 | ```bash 47 | git clone https://github.com/anaskhan28/sieve-repo.git 48 | cd sieve-repo 49 | ``` 50 | 2. **Install dependencies**: 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 3. **Set up environment variables**: Create a `.env` file in the root directory and add the necessary environment variables. 56 | 57 | ``` 58 | NEXT_PUBLIC_SUPABASE_URL=your_supabase_uri 59 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key 60 | YOUTUBE_API_KEY=your_youtube_api_key 61 | ``` 62 | 4. **Run the development server**: 63 | 64 | ```bash 65 | npm run dev 66 | ``` 67 | 68 | ## Usage 🚀 69 | 70 | Once the development server is running, you can: 71 | 72 | - **Access the platform**: Open [sieveit.me](https://sieveit.me) in your browser. 73 | - **Register/Login**: Create a new account or log in to access all features. 74 | - **Browse Playlists**: Explore playlists based on your interests. 75 | - **Rate & Review**: Provide ratings and reviews to share your feedback. 76 | - **Contribute**: Add new playlists to help the community grow. 77 | 78 | ![Sieve Screenshot](public/playlist.png) 79 | 80 | ## License 📄 81 | 82 | Distributed under the MIT License. See `LICENSE` for more information. 83 | 84 | ## Thanks to our Contributors 🫂 85 | 86 | 87 | 88 | 89 | 90 | 91 | Made with [contrib.rocks](https://contrib.rocks). 92 | -------------------------------------------------------------------------------- /app/actions/addPlaylistData.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { PlaylistType } from "@/types/Types"; 3 | import SupabaseServerClient from "@/utils/supabase/server"; 4 | import playlists from "@/playlist.json"; 5 | import { getThumbnailUrl } from './fetchThumbnails'; 6 | 7 | interface JsonPlaylist { 8 | id?: number; 9 | name: string; 10 | playlist_link: string; 11 | summary: string; 12 | title: string; 13 | category: string; 14 | user_profile_link: string; 15 | user_Image: string; 16 | } 17 | 18 | const addOrUpdatePlaylistData = async (): Promise<{ upserted: PlaylistType[], skipped: number } | null> => { 19 | const supabase = await SupabaseServerClient(); 20 | 21 | const { data: { user } } = await supabase.auth.getUser(); 22 | if (!user) { 23 | console.error('No user found'); 24 | return null; 25 | } 26 | 27 | // Fetch all existing playlists from the database 28 | const { data: existingPlaylists, error: fetchError } = await supabase 29 | .from('playlistsInfo') 30 | .select('*'); 31 | 32 | if (fetchError) { 33 | console.error('Error fetching existing playlists:', fetchError); 34 | return null; 35 | } 36 | 37 | const existingPlaylistMap = new Map(existingPlaylists.map(p => [p.playlist_url, p])); 38 | 39 | const playlistsToUpsert: PlaylistType[] = []; 40 | let skippedCount = 0; 41 | 42 | for (const playlist of playlists as JsonPlaylist[]) { 43 | const existingPlaylist = existingPlaylistMap.get(playlist.playlist_link); 44 | console.log(existingPlaylist, 'existing') 45 | const thumbnailUrl = await getThumbnailUrl(playlist.playlist_link); 46 | 47 | const playlistData: any = { 48 | // playlist_id: playlist.id, 49 | user_name: playlist.name, 50 | playlist_url: playlist.playlist_link, 51 | playlist_summary: playlist.summary, 52 | playlist_title: playlist.title, 53 | playlist_category: playlist.category, 54 | playlist_image: thumbnailUrl, 55 | user_profile_link: playlist.user_profile_link, 56 | user_profile_image_link: playlist.user_Image 57 | }; 58 | 59 | if (!existingPlaylist || hasChanges(existingPlaylist, playlistData)) { 60 | console.log(playlistData, 'playlsit-addded') 61 | playlistsToUpsert.push(playlistData); 62 | } else { 63 | skippedCount++; 64 | } 65 | } 66 | 67 | if (playlistsToUpsert.length === 0) { 68 | console.log("No playlists to add or update. Data is up to date."); 69 | return { upserted: [], skipped: skippedCount }; 70 | } 71 | 72 | // Upsert playlists 73 | const { error: upsertError } = await supabase 74 | .from('playlistsInfo') 75 | .upsert(playlistsToUpsert, { onConflict: 'playlist_url', ignoreDuplicates: false }, 76 | 77 | ); 78 | 79 | if (upsertError) { 80 | console.error('Error upserting playlists:', upsertError); 81 | return null; 82 | } 83 | 84 | return { upserted: playlistsToUpsert, skipped: skippedCount }; 85 | } 86 | 87 | function hasChanges(existing: PlaylistType, updated: PlaylistType): boolean { 88 | return Object.keys(updated).some(key => { 89 | if (key === 'playlist_image') { 90 | // Skip comparison for playlist_image as it's dynamically fetched 91 | return false; 92 | } 93 | return existing[key as keyof PlaylistType] !== updated[key as keyof PlaylistType]; 94 | }); 95 | } 96 | 97 | export default addOrUpdatePlaylistData; -------------------------------------------------------------------------------- /app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Image from 'next/image'; 3 | import React, { useEffect } from 'react' 4 | import Link from 'next/link'; 5 | import useSupabaseClient from '@/utils/supabase/client'; 6 | import { Provider } from '@supabase/supabase-js'; 7 | import getUserData from '../actions/getUserData'; 8 | import { redirect } from 'next/navigation'; 9 | type Props = {} 10 | 11 | const Signup = (props: Props) => { 12 | 13 | const supabase = useSupabaseClient(); 14 | 15 | // useEffect(() =>{ 16 | // const getCurrentUser = async() =>{ 17 | // const {data: {session}} = await supabase.auth.getSession(); 18 | 19 | // if(session){ 20 | // redirect('/playlist') 21 | 22 | // } 23 | // }; 24 | // getCurrentUser(); 25 | 26 | 27 | // // console.log(user, 'userdata') 28 | 29 | // }, []); 30 | 31 | 32 | 33 | const socialAuth = async (provider: Provider) =>{ 34 | await supabase.auth.signInWithOAuth({ 35 | provider, 36 | options: { 37 | redirectTo: `${location.origin}/auth/callback`, 38 | } 39 | }) 40 | } 41 | 42 | 43 | return ( 44 |
47 |
50 | sieve 51 | 52 |
53 | sieve 54 | 55 |
56 |
57 | 58 |
59 |
60 | Welcome to Sieve 61 |

Start a journey with us

62 |
63 |
64 | 71 | 78 | Already? Continue Now 80 |
81 | 82 |
83 | 84 | 85 | 86 | 87 |
88 | ) 89 | } 90 | 91 | export default Signup; -------------------------------------------------------------------------------- /.github/scripts/verify_playlist.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jsonschema 3 | import os 4 | 5 | def load_playlists(file_path): 6 | with open(file_path, 'r') as f: 7 | return json.load(f) 8 | 9 | def validate_schema(playlists): 10 | schema = { 11 | "type": "array", 12 | "items": { 13 | "type": "object", 14 | "properties": { 15 | "name": {"type": "string"}, 16 | "playlist_link": {"type": "string", "format": "uri"}, 17 | "summary": {"type": "string"}, 18 | "title": {"type": "string"}, 19 | "category": {"type": "string"}, 20 | "user_profile_link": {"type": "string", "format": "uri"}, 21 | "user_Image": {"type": "string", "format": "uri"} 22 | }, 23 | "required": [ "name", "playlist_link", "summary", "title", "category", "user_profile_link", "user_Image"] 24 | } 25 | } 26 | jsonschema.validate(instance=playlists, schema=schema) 27 | 28 | def check_unique_links(playlists): 29 | links = {} 30 | for playlist in playlists: 31 | link = playlist["playlist_link"] 32 | if link in links: 33 | raise ValueError(f"Duplicate playlist link found: '{link}' (Names: {links[link]}, {playlist['name']})") 34 | links[link] = playlist['title'] 35 | 36 | def check_summary_length(playlists): 37 | for playlist in playlists: 38 | if len(playlist["summary"].split()) < 10: 39 | raise ValueError(f"Summary too short for playlist {playlist['title']}. It should be at least 20 words long.") 40 | 41 | def check_youtube_links(playlists): 42 | for playlist in playlists: 43 | if "youtube.com" not in playlist["playlist_link"] and "youtu.be" not in playlist["playlist_link"]: 44 | raise ValueError(f"Invalid YouTube link for playlist name {playlist['title']}: '{playlist['playlist_link']}'") 45 | 46 | def main(): 47 | errors = [] 48 | try: 49 | playlists = load_playlists('playlist.json') 50 | validate_schema(playlists) 51 | except json.JSONDecodeError as e: 52 | errors.append(f"Invalid JSON format: {str(e)}") 53 | except jsonschema.exceptions.ValidationError as e: 54 | errors.append(f"Schema validation error: {e.message}") 55 | 56 | if not errors: 57 | try: 58 | check_unique_links(playlists) 59 | except ValueError as e: 60 | errors.append(str(e)) 61 | 62 | try: 63 | check_summary_length(playlists) 64 | except ValueError as e: 65 | errors.append(str(e)) 66 | 67 | try: 68 | check_youtube_links(playlists) 69 | except ValueError as e: 70 | errors.append(str(e)) 71 | 72 | # Create the message 73 | if errors: 74 | message = "Playlist verification failed. Please address the following issues:\n\n" 75 | message += "\n".join(f"- {error}" for error in errors) 76 | message += "\n\nPlease review and update your submission." 77 | else: 78 | message = "Playlist verification passed! Your submission looks good." 79 | 80 | # Write result to result.txt using UTF-8 encoding 81 | try: 82 | with open('result.txt', 'w', encoding='utf-8') as fh: 83 | fh.write(message) 84 | except Exception as e: 85 | print(f"Error writing to file: {str(e)}") 86 | # Fallback to printing the message if file writing fails 87 | print(message) 88 | 89 | # For GitHub Actions 90 | print(f"::set-output name=result::{message}") 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /components/Filter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState, useCallback } from 'react' 3 | import { ChevronRight, ChevronLeft } from 'lucide-react'; 4 | import { usePathname } from 'next/navigation'; 5 | import playlists from '@/playlist.json' 6 | import { motion } from 'framer-motion'; 7 | import { useRouter } from 'next/navigation'; 8 | 9 | type Props = { 10 | onFilter: (filter: string) => void; 11 | } 12 | 13 | const filterByCategory = Array.from(new Set(playlists.map((playlist) => playlist.category))); 14 | 15 | const Filter = () => { 16 | const [activeArrow, setActiveArrow] = useState<'left' | 'right' | null>(null); 17 | const [activeButton, setActiveButton] = useState("All"); 18 | const [currentIndex, setCurrentIndex] = useState(0); 19 | const router = useRouter() 20 | 21 | const setFilter =(tag: string) => { 22 | if(tag){ 23 | router.push("?tag=" + tag) 24 | setActiveButton(tag); 25 | } 26 | if(!tag){ 27 | router.push("/playlist") 28 | } 29 | } 30 | 31 | const nextSlide = useCallback(() => { 32 | setCurrentIndex((prevIndex) => (prevIndex + 1) % filterByCategory.length); 33 | setActiveArrow('right'); 34 | setTimeout(() => setActiveArrow(null), 300); 35 | setActiveButton("All") 36 | setFilter("") 37 | 38 | }, []); 39 | 40 | const prevSlide = useCallback(() => { 41 | setCurrentIndex((prevIndex) => 42 | prevIndex === 0 ? filterByCategory.length - 1 : prevIndex - 1 43 | ); 44 | setActiveArrow('left'); 45 | setTimeout(() => setActiveArrow(null), 300); 46 | setActiveButton("All") 47 | setFilter("") 48 | }, []); 49 | 50 | 51 | 52 | return ( 53 |
54 | 60 | 67 | {[...filterByCategory, ...filterByCategory, ...filterByCategory].map((filter, index) => ( 68 | setFilter(filter)} 74 | > 75 | {filter} 76 | 77 | ))} 78 | 79 | 85 |
86 | ) 87 | } 88 | 89 | export default Filter -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import {ArrowUpRight} from 'lucide-react' 3 | 4 | const navigation = { 5 | connect: [ 6 | { name: 'Home', href: '/' }, 7 | { 8 | name: 'Playlist', 9 | href: '/playlist', 10 | }, 11 | { 12 | name: 'SignIn', 13 | href: '/sign', 14 | }, 15 | { 16 | name: 'Join Now', 17 | href: '/sign', 18 | }, 19 | ], 20 | company: [ 21 | { name: 'Guidelines', href: 'https://github.com/anaskhan28/sieve-repo' }, 22 | { name: 'GitHub Repo', href: 'https://github.com/anaskhan28/sieve-repo' }, 23 | 24 | ], 25 | } 26 | 27 | const Footer = () => { 28 | return ( 29 |
34 | 35 |
36 |
37 |
38 | logo 47 |

48 | At Sieve, we believe in the collective wisdom of the tech community. 49 |

50 |
51 |
Made with ❤️ by Anas Khan.
52 |
53 |
54 | {/* Navigations */} 55 |
56 |
57 |

58 | Connect 59 |

60 |
61 | {navigation.connect.map((item) => ( 62 | 72 | ))} 73 |
74 |
75 |
76 |
77 |

78 | Contribute 79 |

80 |
81 | {navigation.company.map((item) => ( 82 | 90 | ))} 91 |
92 |
93 |
94 |
95 |
96 |
97 |

98 | © 2024 Sieve. All rights reserved. 99 |

100 |
101 |
102 |
103 | ) 104 | } 105 | 106 | export default Footer -------------------------------------------------------------------------------- /types/supabase.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json | undefined } 7 | | Json[] 8 | 9 | export type Database = { 10 | public: { 11 | Tables: { 12 | users: { 13 | Row: { 14 | avatar_url: string | null 15 | email: string 16 | full_name: string | null 17 | id: string 18 | } 19 | Insert: { 20 | avatar_url?: string | null 21 | email: string 22 | full_name?: string | null 23 | id: string 24 | } 25 | Update: { 26 | avatar_url?: string | null 27 | email?: string 28 | full_name?: string | null 29 | id?: string 30 | } 31 | Relationships: [ 32 | { 33 | foreignKeyName: "users_id_fkey" 34 | columns: ["id"] 35 | isOneToOne: true 36 | referencedRelation: "users" 37 | referencedColumns: ["id"] 38 | }, 39 | ] 40 | } 41 | } 42 | Views: { 43 | [_ in never]: never 44 | } 45 | Functions: { 46 | [_ in never]: never 47 | } 48 | Enums: { 49 | [_ in never]: never 50 | } 51 | CompositeTypes: { 52 | [_ in never]: never 53 | } 54 | } 55 | } 56 | 57 | type PublicSchema = Database[Extract] 58 | 59 | export type Tables< 60 | PublicTableNameOrOptions extends 61 | | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) 62 | | { schema: keyof Database }, 63 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 64 | ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & 65 | Database[PublicTableNameOrOptions["schema"]]["Views"]) 66 | : never = never, 67 | > = PublicTableNameOrOptions extends { schema: keyof Database } 68 | ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & 69 | Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { 70 | Row: infer R 71 | } 72 | ? R 73 | : never 74 | : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & 75 | PublicSchema["Views"]) 76 | ? (PublicSchema["Tables"] & 77 | PublicSchema["Views"])[PublicTableNameOrOptions] extends { 78 | Row: infer R 79 | } 80 | ? R 81 | : never 82 | : never 83 | 84 | export type TablesInsert< 85 | PublicTableNameOrOptions extends 86 | | keyof PublicSchema["Tables"] 87 | | { schema: keyof Database }, 88 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 89 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] 90 | : never = never, 91 | > = PublicTableNameOrOptions extends { schema: keyof Database } 92 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { 93 | Insert: infer I 94 | } 95 | ? I 96 | : never 97 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] 98 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { 99 | Insert: infer I 100 | } 101 | ? I 102 | : never 103 | : never 104 | 105 | export type TablesUpdate< 106 | PublicTableNameOrOptions extends 107 | | keyof PublicSchema["Tables"] 108 | | { schema: keyof Database }, 109 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 110 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] 111 | : never = never, 112 | > = PublicTableNameOrOptions extends { schema: keyof Database } 113 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { 114 | Update: infer U 115 | } 116 | ? U 117 | : never 118 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] 119 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { 120 | Update: infer U 121 | } 122 | ? U 123 | : never 124 | : never 125 | 126 | export type Enums< 127 | PublicEnumNameOrOptions extends 128 | | keyof PublicSchema["Enums"] 129 | | { schema: keyof Database }, 130 | EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } 131 | ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] 132 | : never = never, 133 | > = PublicEnumNameOrOptions extends { schema: keyof Database } 134 | ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] 135 | : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] 136 | ? PublicSchema["Enums"][PublicEnumNameOrOptions] 137 | : never 138 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { lazy, Suspense, useMemo, useState } from 'react'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | import { useSearchParams } from 'next/navigation'; 6 | import Image from 'next/image'; 7 | import InfiniteScroll from 'react-infinite-scroller'; 8 | import Search from './Search'; 9 | import Filter from './Filter'; 10 | import Loading from './loading'; 11 | import getPlaylistData from '@/app/actions/getPlaylistData'; 12 | import getRatings from '@/app/actions/getRatings'; 13 | import { PlaylistType } from "@/types/Types"; 14 | import addOrUpdatePlaylistData from '@/app/actions/addPlaylistData'; 15 | import playlistJson from '@/playlist.json' 16 | const PlaylistCard = lazy(() => import('./PlaylistCard')); 17 | 18 | // type EnrichedPlaylistType = PlaylistType & { 19 | // playlistRating: number | null; 20 | // avgPlaylistRate: string | null; 21 | // inserted_at: string | undefined; 22 | // }; 23 | 24 | type SortOption = 'rating' | 'newest' | 'oldest'; 25 | 26 | const ClientSideSearchWrapper = () => { 27 | const params = useSearchParams(); 28 | const tag = params.get("tag"); 29 | const search = params.get("search"); 30 | const sort = params.get("sort") as SortOption | null; 31 | 32 | const [displayCount, setDisplayCount] = useState(6); 33 | 34 | const { data: ratingData, error: ratingError } = useQuery({ 35 | queryKey: ["ratings"], 36 | queryFn: getRatings, 37 | }); 38 | 39 | const { 40 | data: playlistData, 41 | error: playlistError, 42 | isLoading 43 | } = useQuery({ 44 | queryKey: ["playlists"], 45 | queryFn: getPlaylistData, 46 | }); 47 | 48 | console.log(playlistError, 'playlistError'); 49 | 50 | 51 | const enrichedPlaylistData = useMemo(() => { 52 | if (!playlistData?.data) return []; 53 | 54 | return playlistData.data.map(playlist => ({ 55 | ...playlist, 56 | playlistRating: ratingData?.find(r => r.playlist_id === playlist.id)?.rating ?? null, 57 | avgPlaylistRate: playlist.playlist_rates?.toFixed(1) ?? null, 58 | inserted_at: playlist?.inserted_at, 59 | })); 60 | }, [playlistData, ratingData]); 61 | 62 | 63 | 64 | const filteredData = useMemo(() => { 65 | let result = enrichedPlaylistData; 66 | 67 | if (tag) { 68 | result = result.filter((playlist) => playlist?.playlist_category === tag); 69 | } 70 | 71 | if (search) { 72 | const searchTerms = search.toLowerCase().split(' '); 73 | result = result.filter((playlist) => 74 | searchTerms.every(term => 75 | playlist.playlist_title.toLowerCase().includes(term) || 76 | playlist.playlist_category?.toLowerCase().includes(term) 77 | ) 78 | ); 79 | } 80 | 81 | if (sort) { 82 | switch (sort) { 83 | case 'rating': 84 | result.sort((a, b) => Number(b.avgPlaylistRate || 0) - Number(a.avgPlaylistRate || 0)); 85 | break; 86 | case 'newest': 87 | result.sort((a, b) => 88 | new Date(b.inserted_at || 0).getTime() - new Date(a.inserted_at || 0).getTime() 89 | ); 90 | break; 91 | case 'oldest': 92 | result.sort((a, b) => 93 | new Date(a.inserted_at || 0).getTime() - new Date(b.inserted_at || 0).getTime() 94 | ); 95 | break; 96 | } 97 | } 98 | 99 | return result; 100 | }, [enrichedPlaylistData, tag, search, sort]); 101 | 102 | console.log(ratingData, 'filtered') 103 | const loadMore = () => { 104 | setDisplayCount((prevCount) => prevCount + 6); 105 | }; 106 | 107 | return ( 108 | <> 109 | 110 | 111 | 112 | {isLoading ? ( 113 |
114 |
115 |
116 | ) : filteredData.length === 0 ? ( 117 |
118 |

No Playlist Found

119 | not-found 120 |
121 | ) : ( 122 | } 127 | className='w-full max-w-7xl' 128 | 129 | > 130 |
131 | {filteredData.slice(0, displayCount).map((playlist, index) => ( 132 | }> 133 | 134 | 135 | ))} 136 |
137 |
138 | )} 139 | 140 | ); 141 | }; 142 | 143 | export default ClientSideSearchWrapper; -------------------------------------------------------------------------------- /components/Rate.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button } from "@/components/ui/button" 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog" 12 | import { useState, useCallback, useEffect } from "react" 13 | import { Star } from 'lucide-react' 14 | import { PlaylistType, RatingType } from "@/types/Types" 15 | 16 | import addRatings from "@/app/actions/addRatings" 17 | import getUserData from "@/app/actions/getUserData" 18 | import getRatings from "@/app/actions/getRatings" 19 | import { revalidatePath } from "next/cache" 20 | import { DialogClose } from "@radix-ui/react-dialog" 21 | import getPlaylistData from "@/app/actions/getPlaylistData" 22 | import Rating from '@/components/Rating'; 23 | import { useOptimistic } from "react" 24 | import { redirect } from 'next/navigation'; 25 | import { useRouter } from 'next/navigation' 26 | 27 | export default function Rate(playlist: PlaylistType) { 28 | const [rating, setRating] = useState(); 29 | const router = useRouter() 30 | 31 | const [optimisticPlaylist, addOptimisticPlaylist] = useOptimistic( 32 | playlist, 33 | (state, newRating: number) => ({ ...state, playlistRating: newRating }) 34 | ); 35 | 36 | function onChange(newValue: number) { 37 | console.log(newValue); 38 | setRating(newValue); 39 | } 40 | 41 | const onSubmit = useCallback(async () => { 42 | const userData: any = await getUserData(); 43 | 44 | if (!userData){ 45 | return router.push('/signup'); 46 | }; 47 | 48 | 49 | 50 | // Optimistically update the UI 51 | addOptimisticPlaylist(rating || 0); 52 | 53 | try { 54 | const ratingData = await addRatings({ 55 | user_id: userData.id, 56 | playlist_id: playlist.id || "", 57 | rating: rating || null 58 | }); 59 | 60 | console.log(ratingData, 'ratingData'); 61 | // You can handle the successful response here if needed 62 | } catch (error) { 63 | console.error('Error submitting rating:', error); 64 | // You might want to revert the optimistic update or show an error message 65 | } 66 | }, [rating, playlist.id,router, addOptimisticPlaylist]); 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | {optimisticPlaylist.playlistRating ? optimisticPlaylist.playlistRating+ ".0" : " "} 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | {rating? rating: playlist.playlistRating ? playlist.playlistRating: "?"} 92 | 93 | 94 |
95 | Rate this 96 | {playlist.playlist_title} 97 |
98 | 99 | {/* ( 100 | 110 | ) */ 111 | } 112 | 113 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 |
132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/process-contribution.yml: -------------------------------------------------------------------------------- 1 | name: Process Contribution 2 | 3 | on: release 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | issues: write 9 | 10 | jobs: 11 | process-contribution: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install jsonschema requests 27 | 28 | - name: Validate JSON and process contribution 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: | 32 | import json 33 | import os 34 | import requests 35 | import sys 36 | import subprocess 37 | 38 | def get_issue_data(issue_number): 39 | url = f"https://api.github.com/repos/${{github.repository}}/issues/{issue_number}" 40 | headers = { 41 | "Authorization": f"token {os.environ['GITHUB_TOKEN']}", 42 | "Accept": "application/vnd.github.v3+json" 43 | } 44 | response = requests.get(url, headers=headers) 45 | if response.status_code != 200: 46 | print(f"Failed to fetch issue data: {response.text}") 47 | sys.exit(1) 48 | return response.json() 49 | 50 | # Get the issue number from the event payload 51 | with open(os.environ['GITHUB_EVENT_PATH'], 'r') as f: 52 | event_data = json.load(f) 53 | issue_number = event_data['issue']['number'] 54 | 55 | # Get the issue data 56 | issue_data = get_issue_data(issue_number) 57 | issue_body = issue_data.get('body', '') 58 | 59 | if not issue_body: 60 | print("Issue body is empty") 61 | sys.exit(1) 62 | 63 | # Extract JSON from issue body 64 | try: 65 | start = issue_body.index('{') 66 | end = issue_body.rindex('}') + 1 67 | json_str = issue_body[start:end] 68 | json_data = json.loads(json_str) 69 | except (ValueError, json.JSONDecodeError) as e: 70 | print(f"Failed to extract JSON from issue body: {e}") 71 | sys.exit(1) 72 | 73 | # Write the extracted JSON to a temporary file 74 | with open('temp_playlist.json', 'w') as f: 75 | json.dump(json_data, f) 76 | 77 | # Run the verify_playlist.py script 78 | result = subprocess.run(['python', '.github/scripts/verify_playlist.py', 'temp_playlist.json'], capture_output=True, text=True) 79 | 80 | if result.returncode != 0: 81 | print(f"Playlist verification failed: {result.stdout}\n{result.stderr}") 82 | sys.exit(1) 83 | 84 | print("Playlist verification passed") 85 | 86 | # If validation passes, append to playlist.json 87 | with open('playlist.json', 'r+') as f: 88 | playlists = json.load(f) 89 | playlists.append(json_data) 90 | f.seek(0) 91 | json.dump(playlists, f, indent=2) 92 | 93 | # Create a new branch 94 | branch_name = f"contribution-{issue_number}" 95 | os.system(f"git checkout -b {branch_name}") 96 | os.system("git add playlist.json") 97 | os.system('git config user.name "github-actions[bot]"') 98 | os.system('git config user.email "github-actions[bot]@users.noreply.github.com"') 99 | os.system(f'git commit -m "Add contribution from issue #{issue_number}"') 100 | os.system(f"git push origin {branch_name}") 101 | 102 | # Create a pull request 103 | pr_url = f"https://api.github.com/repos/${{github.repository}}/pulls" 104 | pr_data = { 105 | "title": f"Contribution from issue #{issue_number}", 106 | "body": f"This PR adds the contribution from issue #{issue_number}", 107 | "head": branch_name, 108 | "base": "main" 109 | } 110 | 111 | headers = { 112 | "Authorization": f"token {os.environ['GITHUB_TOKEN']}", 113 | "Accept": "application/vnd.github.v3+json" 114 | } 115 | response = requests.post(pr_url, json=pr_data, headers=headers) 116 | if response.status_code == 201: 117 | print(f"Pull request created successfully: {response.json()['html_url']}") 118 | else: 119 | print(f"Failed to create pull request: {response.text}") 120 | sys.exit(1) 121 | 122 | 123 | shell: python 124 | 125 | - name: Comment on issue 126 | if: success() 127 | uses: actions/github-script@v6 128 | with: 129 | github-token: ${{secrets.GITHUB_TOKEN}} 130 | script: | 131 | 132 | await github.rest.issues.createComment({ 133 | ...context.repo, 134 | issue_number: context.issue.number, 135 | owner: context.repo.owner, 136 | repo: context.repo.name, 137 | body: 'Thank you for your contribution! A pull request has been created with your changes. The repository owner will review and merge it soon.' 138 | }); 139 | 140 | - name: Comment on issue if failed 141 | if: failure() 142 | uses: actions/github-script@v6 143 | with: 144 | github-token: ${{secrets.GITHUB_TOKEN}} 145 | script: | 146 | await github.rest.issues.createComment({ 147 | issue_number: context.issue.number, 148 | owner: context.repo.owner, 149 | repo: context.repo.name, 150 | body: 'There was an error processing your contribution. Please check that your JSON is correctly formatted and includes all required fields.' 151 | }) 152 | -------------------------------------------------------------------------------- /.github/workflows/add-playlist-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Process Contribution New 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | issues: write 11 | 12 | jobs: 13 | process-contribution: 14 | if: contains(github.event.issue.labels.*.name, 'playlist-submission') 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.x' 25 | 26 | - name: Install dependencies 27 | run: | 28 | sudo apt update && sudo apt install jq curl -y 29 | python -m pip install --upgrade pip 30 | pip install jsonschema requests 31 | 32 | - name: Validate JSON and process contribution 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | GITHUB_CONTEXT: ${{ toJson(github) }} 36 | EVENT_ID: ${{ github.event.issue.number }} 37 | ISSUE_BODY: ${{ github.event.issue.body }} 38 | run: | 39 | BODY="$ISSUE_BODY" 40 | PLAYLIST_TITLE=$(echo "$BODY" | grep "### Playlist Title" -A 2 | tail -n 1) 41 | PLAYLIST_URL=$(echo "$BODY" | grep "### YouTube Playlist URL" -A 2 | tail -n 1) 42 | PLAYLIST_SUMMARY=$(echo "$BODY" | sed -n '/### Playlist Summary/,/### Category/{//!p}' | sed '$d') 43 | PLAYLIST_CATEGORY=$(echo "$BODY" | grep "Category" -A 2 | tail -n 1) 44 | CREATOR="${{ github.event.issue.user.login }}" 45 | 46 | echo """ 47 | { 48 | \"name\": \"$CREATOR\", 49 | \"playlist_link\": \"$PLAYLIST_URL\", 50 | \"summary\": \"$PLAYLIST_SUMMARY\", 51 | \"title\": \"$PLAYLIST_TITLE\", 52 | \"category\": \"$PLAYLIST_CATEGORY\", 53 | \"user_profile_link\": \"https://github.com/$CREATOR\", 54 | \"user_Image\": \"https://avatars.githubusercontent.com/$CREATOR\" 55 | } 56 | """ | tee temp_playlist.json 57 | 58 | 59 | # # Set outputs 60 | # echo "creator=$CREATOR" >> $GITHUB_OUTPUT 61 | # echo "playlist_title=$PLAYLIST_TITLE" >> $GITHUB_OUTPUT 62 | 63 | # Verify the playlist 64 | python .github/scripts/verify_playlist.py temp_playlist.json 65 | if [[ $? -ne 0 ]]; then 66 | echo "Playlist verification failed" 67 | exit 1 68 | fi 69 | echo "Playlist verification passed" 70 | # Append to playlist.json 71 | if [[ -f "playlist.json" ]]; then 72 | json_data=$( playlist_temp.json && mv playlist_temp.json playlist.json 74 | else 75 | echo "playlist.json not found!" 76 | exit 1 77 | fi 78 | 79 | # Create a new branch 80 | issue_number=$EVENT_ID 81 | branch_name="contribution-$issue_number" 82 | git checkout -b "$branch_name" 83 | git add playlist.json 84 | git config user.name "github-actions[bot]" 85 | git config user.email "github-actions[bot]@users.noreply.github.com" 86 | git commit -m "Add contribution from issue #$issue_number" 87 | git push origin "$branch_name" 88 | 89 | # Create a pull request 90 | pr_url="https://api.github.com/repos/${{github.repository}}/pulls" 91 | pr_data=$(jq -n --arg title "Contribution from issue #$issue_number" \ 92 | --arg body "$(printf "### Description\n\nThis PR adds the contribution from issue #$issue_number by @%s.\n\n#### Playlist Details:\n\`\`\`json\n%s\n\`\`\`" "$CREATOR" "$(cat temp_playlist.json)")" \ 93 | --arg head "$branch_name" \ 94 | --arg base "main" \ 95 | '{title: $title, body: $body, head: $head, base: $base}') 96 | 97 | response=$(curl -s -w "%{http_code}" -o response.json -X POST \ 98 | -H "Authorization: token $GITHUB_TOKEN" \ 99 | -H "Accept: application/vnd.github.v3+json" \ 100 | -d "$pr_data" \ 101 | "$pr_url") 102 | 103 | http_code=${response:(-3)} 104 | if [[ $http_code -eq 201 ]]; then 105 | pr_link=$(jq -r '.html_url' response.json) 106 | echo "Pull request created successfully: $pr_link" 107 | else 108 | echo "Failed to create pull request" 109 | jq '.' response.json 110 | exit 1 111 | fi 112 | 113 | 114 | shell: bash 115 | 116 | - name: Comment on issue 117 | if: success() 118 | uses: actions/github-script@v6 119 | with: 120 | github-token: ${{secrets.GITHUB_TOKEN}} 121 | script: | 122 | 123 | try { 124 | await github.rest.issues.createComment({ 125 | issue_number: context.issue.number, 126 | owner: context.repo.owner, 127 | repo: context.repo.repo, 128 | body: 'Thank you for your contribution! A pull request has been created with your changes. The repository owner will review and merge it soon.' 129 | }); 130 | } catch (error) { 131 | core.setFailed('Failed to post comment'); 132 | } 133 | 134 | - name: Comment on issue if failed 135 | if: failure() 136 | uses: actions/github-script@v6 137 | with: 138 | github-token: ${{secrets.GITHUB_TOKEN}} 139 | script: | 140 | 141 | try { 142 | await github.rest.issues.createComment({ 143 | issue_number: context.issue.number, 144 | owner: context.repo.owner, 145 | repo: context.repo.repo, 146 | body: 'There was an error processing your contribution. Please check that your JSON is correctly formatted and includes all required fields.' 147 | }); 148 | } catch (error) { 149 | core.setFailed('Failed to post comment'); 150 | } 151 | -------------------------------------------------------------------------------- /public/sieve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/utils/cn" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState, useEffect } from 'react' 3 | import { motion, AnimatePresence } from 'framer-motion' 4 | import Link from 'next/link' 5 | import Image from 'next/image' 6 | import NameLogo from '@/public/name.png' 7 | import useSupabaseClient from '@/utils/supabase/client' 8 | import { User } from '@supabase/supabase-js' 9 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 10 | import { usePathname } from 'next/navigation' 11 | 12 | const Navbar = () => { 13 | const [isOpen, setIsOpen] = useState(false) 14 | const [user, setUser] = useState() 15 | const [isMounted, setIsMounted] = useState(false) 16 | const supabase = useSupabaseClient() 17 | const pathName = usePathname() 18 | 19 | useEffect(() => { 20 | const getCurrentUser = async () => { 21 | const { data: { session } } = await supabase.auth.getSession() 22 | if (session) { 23 | setUser(session.user) 24 | } 25 | } 26 | getCurrentUser() 27 | setIsMounted(true) 28 | }, []) 29 | 30 | if (!isMounted) return null 31 | 32 | const handleSignout = async () => { 33 | const { error } = await supabase.auth.signOut() 34 | if (!error) setUser(undefined) 35 | } 36 | 37 | const userProfile = user?.user_metadata?.avatar_url 38 | 39 | return ( 40 | <> 41 | {/* Desktop Navbar */} 42 | 91 | 92 | {/* Mobile Navbar */} 93 | 154 | 155 | ) 156 | } 157 | 158 | export default Navbar 159 | 160 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the 2 | # working directory name when running `supabase init`. 3 | project_id = "sieve" 4 | 5 | [api] 6 | enabled = true 7 | # Port to use for the API URL. 8 | port = 54321 9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 10 | # endpoints. `public` is always included. 11 | schemas = ["public", "graphql_public"] 12 | # Extra schemas to add to the search_path of every request. `public` is always included. 13 | extra_search_path = ["public", "extensions"] 14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 15 | # for accidental or malicious requests. 16 | max_rows = 1000 17 | 18 | [db] 19 | # Port to use for the local database URL. 20 | port = 54322 21 | # Port used by db diff command to initialize the shadow database. 22 | shadow_port = 54320 23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 24 | # server_version;` on the remote database to check. 25 | major_version = 15 26 | 27 | [db.pooler] 28 | enabled = false 29 | # Port to use for the local connection pooler. 30 | port = 54329 31 | # Specifies when a server connection can be reused by other clients. 32 | # Configure one of the supported pooler modes: `transaction`, `session`. 33 | pool_mode = "transaction" 34 | # How many server connections to allow per user/database pair. 35 | default_pool_size = 20 36 | # Maximum number of client connections allowed. 37 | max_client_conn = 100 38 | 39 | [realtime] 40 | enabled = true 41 | # Bind realtime via either IPv4 or IPv6. (default: IPv4) 42 | # ip_version = "IPv6" 43 | # The maximum length in bytes of HTTP request headers. (default: 4096) 44 | # max_header_length = 4096 45 | 46 | [studio] 47 | enabled = true 48 | # Port to use for Supabase Studio. 49 | port = 54323 50 | # External URL of the API server that frontend connects to. 51 | api_url = "http://127.0.0.1" 52 | # OpenAI API Key to use for Supabase AI in the Supabase Studio. 53 | openai_api_key = "env(OPENAI_API_KEY)" 54 | 55 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 56 | # are monitored, and you can view the emails that would have been sent from the web interface. 57 | [inbucket] 58 | enabled = true 59 | # Port to use for the email testing server web interface. 60 | port = 54324 61 | # Uncomment to expose additional ports for testing user applications that send emails. 62 | # smtp_port = 54325 63 | # pop3_port = 54326 64 | 65 | [storage] 66 | enabled = true 67 | # The maximum file size allowed (e.g. "5MB", "500KB"). 68 | file_size_limit = "50MiB" 69 | 70 | [storage.image_transformation] 71 | enabled = true 72 | 73 | [auth] 74 | enabled = true 75 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 76 | # in emails. 77 | site_url = "http://127.0.0.1:3000" 78 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 79 | additional_redirect_urls = ["https://127.0.0.1:3000"] 80 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). 81 | jwt_expiry = 3600 82 | # If disabled, the refresh token will never expire. 83 | enable_refresh_token_rotation = true 84 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. 85 | # Requires enable_refresh_token_rotation = true. 86 | refresh_token_reuse_interval = 10 87 | # Allow/disallow new user signups to your project. 88 | enable_signup = true 89 | # Allow/disallow anonymous sign-ins to your project. 90 | enable_anonymous_sign_ins = false 91 | # Allow/disallow testing manual linking of accounts 92 | enable_manual_linking = false 93 | 94 | [auth.email] 95 | # Allow/disallow new user signups via email to your project. 96 | enable_signup = true 97 | # If enabled, a user will be required to confirm any email change on both the old, and new email 98 | # addresses. If disabled, only the new email is required to confirm. 99 | double_confirm_changes = true 100 | # If enabled, users need to confirm their email address before signing in. 101 | enable_confirmations = false 102 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. 103 | max_frequency = "1s" 104 | 105 | # Uncomment to customize email template 106 | # [auth.email.template.invite] 107 | # subject = "You have been invited" 108 | # content_path = "./supabase/templates/invite.html" 109 | 110 | [auth.sms] 111 | # Allow/disallow new user signups via SMS to your project. 112 | enable_signup = true 113 | # If enabled, users need to confirm their phone number before signing in. 114 | enable_confirmations = false 115 | # Template for sending OTP to users 116 | template = "Your code is {{ .Code }} ." 117 | # Controls the minimum amount of time that must pass before sending another sms otp. 118 | max_frequency = "5s" 119 | 120 | # Use pre-defined map of phone number to OTP for testing. 121 | # [auth.sms.test_otp] 122 | # 4152127777 = "123456" 123 | 124 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. 125 | # [auth.hook.custom_access_token] 126 | # enabled = true 127 | # uri = "pg-functions:////" 128 | 129 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. 130 | [auth.sms.twilio] 131 | enabled = false 132 | account_sid = "" 133 | message_service_sid = "" 134 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: 135 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" 136 | 137 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 138 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, 139 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 140 | [auth.external.apple] 141 | enabled = false 142 | client_id = "" 143 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: 144 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" 145 | # Overrides the default auth redirectUrl. 146 | redirect_uri = "" 147 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 148 | # or any other third-party OIDC providers. 149 | url = "" 150 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. 151 | skip_nonce_check = false 152 | 153 | [edge_runtime] 154 | enabled = true 155 | # Configure one of the supported request policies: `oneshot`, `per_worker`. 156 | # Use `oneshot` for hot reload, or `per_worker` for load testing. 157 | policy = "oneshot" 158 | inspector_port = 8083 159 | 160 | [analytics] 161 | enabled = false 162 | port = 54327 163 | vector_port = 54328 164 | # Configure one of the supported backends: `postgres`, `bigquery`. 165 | backend = "postgres" 166 | 167 | # Experimental features may be deprecated any time 168 | [experimental] 169 | # Configures Postgres storage engine to use OrioleDB (S3) 170 | orioledb_version = "" 171 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com 172 | s3_host = "env(S3_HOST)" 173 | # Configures S3 bucket region, eg. us-east-1 174 | s3_region = "env(S3_REGION)" 175 | # Configures AWS_ACCESS_KEY_ID for S3 bucket 176 | s3_access_key = "env(S3_ACCESS_KEY)" 177 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket 178 | s3_secret_key = "env(S3_SECRET_KEY)" 179 | -------------------------------------------------------------------------------- /app/playlist/[playlistId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useMemo } from "react"; 4 | import playlists from "@/playlist.json"; 5 | import PlaylistCard from "@/components/PlaylistCard"; 6 | import Image from "next/image"; 7 | import { Star, Eye } from "lucide-react"; 8 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 9 | import Link from "next/link"; 10 | import getPlaylistData from "@/app/actions/getPlaylistData"; 11 | import Rate from "@/components/Rate"; 12 | import getRatings from "@/app/actions/getRatings"; 13 | import PlaylistCards from "@/components/PlaylistCards"; 14 | import getUserData from "@/app/actions/getUserData"; 15 | import { redirect } from "next/navigation"; 16 | import { PlaylistType } from "@/types/Types"; 17 | import Filter from "@/components/Filter"; 18 | import { getPlaylistCardData } from "@/utils/getPlaylistCardData"; 19 | import { useQuery } from "@tanstack/react-query"; 20 | import { getQueryClient } from "@/utils/query"; 21 | 22 | 23 | type EnrichedPlaylistType = PlaylistType & { 24 | playlistRating: number | null; 25 | avgPlaylistRate: string | null; 26 | }; 27 | 28 | const PlaylistDetail = ({ 29 | params, 30 | }: { 31 | params: { playlistId: string }; 32 | }) => { 33 | // const userData = await getUserData(); 34 | 35 | // if (!userData) { 36 | // redirect('/signup'); 37 | // } 38 | 39 | const { data: ratingData, error: ratingError } = useQuery({ 40 | queryKey: ["ratings"], 41 | queryFn: getRatings, 42 | }); 43 | 44 | const { 45 | data: playlistData, 46 | error: playlistError, 47 | isLoading 48 | } = useQuery({ 49 | queryKey: ["playlists"], 50 | queryFn: getPlaylistData, 51 | }); 52 | 53 | console.log(playlistData, 'playlsitData') 54 | 55 | const enrichedPlaylistData = useMemo(() => { 56 | if (!playlistData?.data) return []; 57 | 58 | return playlistData.data.map(playlist => ({ 59 | ...playlist, 60 | playlistRating: ratingData?.find(r => r.playlist_id === playlist.id)?.rating ?? null, 61 | avgPlaylistRate: playlist.playlist_rates?.toFixed(1) ?? null, 62 | inserted_at: playlist?.inserted_at, 63 | })); 64 | }, [playlistData, ratingData]); 65 | 66 | console.log(enrichedPlaylistData, 'enrichedPlaylistData') 67 | 68 | // const dataPlaylist = await getPlaylistCardData(); 69 | 70 | 71 | // const { 72 | // data: playlistData, 73 | // error: playlistError, 74 | // isLoading 75 | // } = useQuery({ 76 | // queryKey: ["playlists"], 77 | // queryFn: getPlaylistData, 78 | // }); 79 | 80 | // const queryClient = getQueryClient() 81 | 82 | // const playlistCardData = await queryClient.getQueryData(['playlists']); 83 | 84 | // console.log(playlistCardData, 'data-playlist') 85 | 86 | // if (!dataPlaylist) return null; 87 | 88 | // const enrichedPlaylists: EnrichedPlaylistType[] = dataPlaylist.playlistData.data!.map(playlist => ({ 89 | // ...playlist, 90 | // playlistRating: dataPlaylist.ratings.find(r => r.playlist_id === playlist.id)?.rating || null, 91 | // avgPlaylistRate: playlist.playlist_rates?.toFixed(1) || null 92 | // })); 93 | 94 | const selectedPlaylist = enrichedPlaylistData.find(pl => pl.id === params.playlistId); 95 | 96 | if (!selectedPlaylist) { 97 | return

Playlist not found.

; 98 | } 99 | const filteredPlaylists = enrichedPlaylistData.filter(pl => pl.id !== params.playlistId); 100 | const sortedPlaylists = filteredPlaylists.sort((a, b) => Number(b.avgPlaylistRate) - Number(a.avgPlaylistRate)); 101 | 102 | 103 | return ( 104 |
105 |
106 |
107 |
108 | {selectedPlaylist.playlist_title} 114 |
115 | 116 | play 117 | 118 |
119 |
120 |
121 | 122 | {selectedPlaylist.playlist_title} 123 | 124 | 125 | 126 | 132 | {selectedPlaylist.playlist_rates?.toFixed(1) || 0.0} 133 | 134 | 135 | {/*
136 | 2.1k 137 | 138 |
*/} 139 |
140 |
141 | 142 | 146 | 147 | profile 153 | 154 | 155 | 156 |
157 |
158 |

159 | Contributed by 160 |

161 | 162 |

163 | {selectedPlaylist.user_name} 164 |

165 | 166 |
167 |

168 | {selectedPlaylist.playlist_summary} 169 |

170 |
171 |
172 |
173 |
174 | {/*

175 | Most Rated Playlists 176 |

*/} 177 | 178 |
179 | 183 |
184 |
185 |
186 |
187 | ); 188 | }; 189 | 190 | export default PlaylistDetail; 191 | -------------------------------------------------------------------------------- /components/NavbarMobile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useEffect, useState } from 'react' 3 | import Link from 'next/link' 4 | import Image from 'next/image' 5 | import NameLogo from '@/public/name.png' 6 | import useSupabaseClient from '@/utils/supabase/client'; 7 | import {User} from '@supabase/supabase-js' 8 | 9 | import { 10 | Avatar, 11 | AvatarFallback, 12 | AvatarImage, 13 | } from "@/components/ui/avatar" 14 | import { usePathname } from 'next/navigation' 15 | 16 | import { Youtube, Home, LogOut, CircleUserRound, LogIn } from 'lucide-react'; 17 | 18 | 19 | export function NavbarMobile() { 20 | const [user, setUser] = useState(); 21 | const [isMounted, setIsMounted] = useState(false); 22 | const supabase = useSupabaseClient(); 23 | 24 | const pathName = usePathname(); 25 | 26 | 27 | 28 | const handleSignout = async () =>{ 29 | const {error} = await supabase.auth.signOut(); 30 | if(!error) setUser(undefined) 31 | window.location.reload(); 32 | 33 | } 34 | 35 | 36 | 37 | 38 | 39 | useEffect(() =>{ 40 | const getCurrentUser = async() =>{ 41 | const {data: {session},} = await supabase.auth.getSession(); 42 | 43 | if(session){ 44 | setUser(session.user) 45 | } 46 | }; 47 | getCurrentUser(); 48 | 49 | setIsMounted(true); 50 | 51 | // console.log(user, 'userdata') 52 | 53 | }, []); 54 | 55 | if(!isMounted) return null 56 | const userProfile = user?.user_metadata?.avatar_url 57 | console.log(userProfile, 'userProfile') 58 | 59 | 60 | return ( 61 | <> 62 | 120 | 121 |
122 | 123 | 128 | 129 | 130 | 131 | Playlists 132 | 133 | 134 | {!user && ( 135 | <> 136 | 141 | 142 | Signup 143 | 144 | 145 | 146 | 151 | 152 | Login 153 | 154 | 155 | )} 156 | {user && ( 157 | <> 158 | 159 | 164 | 165 | Rated Playlists 166 | 167 | 168 | 173 | 174 | Contribute 175 | 176 | 177 | 188 | 189 | 194 | 195 | 196 | profile 197 | 198 | 199 | 200 | 201 | )} 202 | 203 | 204 | 205 | 206 |
207 | 208 | ) 209 | } 210 | 211 | -------------------------------------------------------------------------------- /public/signup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /playlist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Faraz", 4 | "playlist_link": "https://www.youtube.com/watch?v=T6atrhxLRkI&list=PLlHtucAD9KT33e6ScNCtgVPS5kXlLOu0O&pp=iAQB", 5 | "summary": "This playlist covers the fundamentals of UX and UI design for tech apps. It provides essential knowledge for beginners in the field of design. The content focuses on practical skills needed to design user-friendly applications.", 6 | "title": "Basics of UX, UI Design", 7 | "category": "UI UX Design", 8 | "user_profile_link": "https://github.com/ahmedfarazzz", 9 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 10 | }, 11 | { 12 | "name": "Faraz", 13 | "playlist_link": "https://www.youtube.com/watch?v=ajdRvxDWH4w&list=PLGjplNEQ1it_oTvuLRNqXfz_v_0pq6unW", 14 | "summary": "A comprehensive JavaScript tutorial suitable for beginners. Covers fundamental concepts and progresses to advanced topics. Ideal for those looking to build a strong foundation in JavaScript programming.", 15 | "title": "Basics to Adcance Javascript - Beginner's Guide", 16 | "category": "Frontend Development", 17 | "user_profile_link": "https://github.com/ahmedfarazzz", 18 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 19 | }, 20 | { 21 | "name": "Faraz", 22 | "playlist_link": "https://www.youtube.com/watch?v=WZQYt5HZ0-E&list=PLDtHAiqIa4wa5MBbE_XDoqY51sAkQnkjt&pp=iAQB", 23 | "summary": "An intensive course for aspiring designers seeking a career in design. Covers essential design principles, tools, and techniques. Provides practical insights and industry-relevant skills for design professionals.", 24 | "title": "Design Crash Course - ABNUX", 25 | "category": "UI UX Design", 26 | "user_profile_link": "https://github.com/ahmedfarazzz", 27 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 28 | }, 29 | { 30 | "name": "Faraz", 31 | "playlist_link": "https://www.youtube.com/watch?v=vd1vRpoWC3M&list=PLW-zSkCnZ-gCq0DjkzY-YapCBEk0lA6lR", 32 | "summary": "A thorough tutorial on Adobe Illustrator, covering beginner to advanced levels. Teaches vector graphics creation and manipulation techniques. Offers practical projects and exercises to enhance illustration skills.", 33 | "title": "Adobe Illustrator - GFX Mentor ", 34 | "category": "UI UX Design", 35 | "user_profile_link": "https://github.com/ahmedfarazzz", 36 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 37 | }, 38 | { 39 | "name": "Faraz", 40 | "playlist_link": "https://www.youtube.com/watch?v=Xv8JBXPgeI8&list=PLW-zSkCnZ-gD8OcjTPu-u_Rxl9-kI9Xqr", 41 | "summary": "Comprehensive guide to Adobe After Effects from beginner to advanced levels. Covers motion graphics, visual effects, and animation techniques. Includes practical projects to build a strong portfolio in video post-production.", 42 | "title": "Adobe After Effects - GFX Mentor", 43 | "category": "UI UX Design", 44 | "user_profile_link": "https://github.com/ahmedfarazzz", 45 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 46 | }, 47 | { 48 | "name": "Faraz", 49 | "playlist_link": "https://www.youtube.com/watch?v=aVCz8pEJwTk&list=PLW-zSkCnZ-gDRQxW05I3nhwE6F25Lr98Q&pp=iAQB", 50 | "summary": "Advanced-level course on Adobe Premiere Pro for video editing. Focuses on professional-grade techniques and workflows. Covers color grading, audio editing, and advanced effects for high-quality video production.", 51 | "title": "Adobe Premiere Pro - GFX Mentor", 52 | "category": "UI UX Design", 53 | "user_profile_link": "https://github.com/ahmedfarazzz", 54 | "user_Image": "https://avatars.githubusercontent.com/ahmedfarazzz" 55 | }, 56 | { 57 | "name": "Ayaan Shaikh", 58 | "playlist_link": "https://www.youtube.com/playlist?list=PLC3y8-rFHvwjOKd6gdf4QtV1uYNiQnruI", 59 | "summary": "Comprehensive tutorial covering Next.js 14 features and updates. Provides in-depth explanations of new functionalities and best practices. Ideal for developers looking to master the latest version of Next.js framework.", 60 | "title": "Next.js 14 Tutorial", 61 | "category": "Frontend Development", 62 | "user_profile_link": "https://github.com/Ayaanshaikh90", 63 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 64 | }, 65 | { 66 | "name": "Ayaan Shaikh", 67 | "playlist_link": "https://www.youtube.com/playlist?list=PLpPqplz6dKxW5ZfERUPoYTtNUNvrEebAR", 68 | "summary": "Beginner-friendly ReactJS course covering fundamental concepts. Includes hands-on examples and projects to reinforce learning. Builds a strong foundation for developing modern web applications with React.", 69 | "title": "ReactJS Course For Beginners", 70 | "category": "Frontend Development", 71 | "user_profile_link": "https://github.com/Ayaanshaikh90", 72 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 73 | }, 74 | { 75 | "name": "Ayaan Shaikh", 76 | "playlist_link": "https://www.youtube.com/playlist?list=PL4-IK0AVhVjOSNeNSB0hAVMmRB102o47u", 77 | "summary": "Simplified explanations of CSS Grid and Flexbox properties. Offers practical tips and tricks for effective layout design. Helps developers master modern CSS layout techniques for responsive web design.", 78 | "title": "Grid tips and tricks", 79 | "category": "Frontend Development", 80 | "user_profile_link": "https://github.com/Ayaanshaikh90", 81 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 82 | }, 83 | { 84 | "name": "Ayaan Shaikh", 85 | "playlist_link": "https://www.youtube.com/playlist?list=PLZPZq0r_RZOMRMjHB_IEBjOW_ufr00yG1", 86 | "summary": "Comprehensive JavaScript tutorial for beginners with in-depth concept explanations. Covers core language features, DOM manipulation, and basic algorithms. Includes practical exercises to reinforce learning and build coding skills.", 87 | "title": "JavaScript tutorial for beginners", 88 | "category": "Frontend Development", 89 | "user_profile_link": "https://github.com/Ayaanshaikh90", 90 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 91 | }, 92 | { 93 | "name": "Ayaan Shaikh", 94 | "playlist_link": "https://www.youtube.com/watch?v=BCQHnlnPusY&list=PLRqwX-V7Uu6ZF9C0YMKuns9sLDzK6zoiV", 95 | "summary": "Beginner-friendly tutorial on Git and GitHub version control systems. Covers essential commands, workflows, and collaboration techniques. Provides practical examples for effective project management and team collaboration.", 96 | "title": "Git & Github", 97 | "category": "Backend Development", 98 | "user_profile_link": "https://github.com/Ayaanshaikh90", 99 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 100 | }, 101 | { 102 | "name": "Ayaan Shaikh", 103 | "playlist_link": "https://www.youtube.com/watch?v=iqYK2UgNv-w&list=PLLdz3KlabJv3U7Hdibr4S8VgM0-NQm-h2", 104 | "summary": "Simplified React course designed for easy understanding of core concepts. Covers components, state management, and hooks in a straightforward manner. Includes practical projects to apply learned concepts in real-world scenarios.", 105 | "title": "Simplified React Course", 106 | "category": "Frontend Development", 107 | "user_profile_link": "https://github.com/Ayaanshaikh90", 108 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 109 | }, 110 | { 111 | "name": "Ayaan Shaikh", 112 | "playlist_link": "https://www.youtube.com/playlist?list=PLbtI3_MArDOlJ4036mWiUKaQToUS8MZVu", 113 | "summary": "Comprehensive series to master Git and GitHub version control. Covers advanced topics like branching strategies, merge conflict resolution, and CI/CD integration. Provides in-depth knowledge for efficient team collaboration and project management.", 114 | "title": "Git & Github", 115 | "category": "Backend Development", 116 | "user_profile_link": "https://github.com/Ayaanshaikh90", 117 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 118 | }, 119 | { 120 | "name": "Ayaan Shaikh", 121 | "playlist_link": "https://www.youtube.com/playlist?list=PLbtI3_MArDOnIIJxB6xFtpnhM0wTwz0x6", 122 | "summary": "Comprehensive course on GSAP (GreenSock Animation Platform) for web animations. Covers basic to advanced animation techniques for creating engaging web experiences. Includes practical projects to master timeline animations, easing, and plugin usage.", 123 | "title": "GSAP Animation", 124 | "category": "Animation", 125 | "user_profile_link": "https://github.com/Ayaanshaikh90", 126 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 127 | }, 128 | { 129 | "name": "Ayaan Shaikh", 130 | "playlist_link": "https://www.youtube.com/playlist?list=PL0-xwzAwzllw_dvNfabV28-bpAEoMchd3", 131 | "summary": "Step-by-step guide to AWS security best practices and implementations. Covers IAM, VPC security, encryption, and compliance in the AWS ecosystem. Provides hands-on examples for securing cloud infrastructure and applications.", 132 | "title": "AWS Security", 133 | "category": "Devops", 134 | "user_profile_link": "https://github.com/Ayaanshaikh90", 135 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 136 | }, 137 | { 138 | "name": "Ayaan Shaikh", 139 | "playlist_link": "https://www.youtube.com/playlist?list=PLhnVDNT5zYN97qxG4J_V_9Di8xT9_QDvZ", 140 | "summary": "Comprehensive guide to NextAuth for authentication in Next.js applications. Covers setup, configuration, and implementation of various authentication providers. Includes advanced topics like custom callbacks, JWT handling, and database integration.", 141 | "title": "Next Auth", 142 | "category": "Frontend Development", 143 | "user_profile_link": "https://github.com/Ayaanshaikh90", 144 | "user_Image": "https://avatars.githubusercontent.com/Ayaanshaikh90" 145 | }, 146 | { 147 | "name": "Anas Khan", 148 | "playlist_link": "https://youtu.be/f9Aje_cN_CY?si=XUUXhEABPmCTfAl-", 149 | "summary": "Comprehensive tutorial on Data Structures and Algorithms using Python. Covers fundamental data structures like LinkedList, Arrays, Queue, and Stack. Includes implementation details and practical examples for each data structure.", 150 | "title": "Data Structures and Algorithms using Python", 151 | "category": "Data Structures", 152 | "user_profile_link": "https://github.com/anaskhan28", 153 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 154 | }, 155 | { 156 | "name": "Anas Khan", 157 | "playlist_link": "https://www.youtube.com/playlist?list=PLB0Tybl0UNfaT1uq9anIjt_Ggo7mT6mMb", 158 | "summary": "Curated playlist of core JavaScript project tutorials for hands-on learning. Covers a variety of practical projects to reinforce JavaScript concepts. Provides real-world application of JavaScript skills for portfolio building.", 159 | "title": "JavaScript Project Tutorials", 160 | "category": "Projects", 161 | "user_profile_link": "https://github.com/anaskhan28", 162 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 163 | }, 164 | { 165 | "name": "Anas Khan", 166 | "playlist_link": "https://youtu.be/5r25Y9Vg2P4?si=TndKCWnyHzYM6bmM", 167 | "summary": "It follows the react best practices for React development, emphasizing the importance of avoiding magic values, maintaining a clear folder structure, creating components thoughtfully, and minimizing unnecessary markup.", 168 | "title": "React Best Practices", 169 | "category": "Frontend Development", 170 | "user_profile_link": "https://github.com/anaskhan28", 171 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 172 | }, 173 | { 174 | "name": "Anas Khan", 175 | "playlist_link": "https://www.youtube.com/playlist?list=PLIY8eNdw5tW_7-QrsY_n9nC0Xfhs1tLEK", 176 | "summary": "Comprehensive course on Network Security for IT students. Covers essential concepts, algorithms, and protocols in cybersecurity. Provides practical knowledge for implementing secure network architectures.", 177 | "title": "Network Security", 178 | "category": "Cyber Security", 179 | "user_profile_link": "https://github.com/anaskhan28", 180 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 181 | }, 182 | { 183 | "name": "Swati Tiwari", 184 | "playlist_link": "https://www.youtube.com/watch?v=4s1Tcvm00pA&list=PL9gnSGHSqcnqfctdbCQKaw5oZ9Up2cmsq", 185 | "summary": "In-depth tutorial on tree data structures and their implementations. Covers various types of trees, traversal algorithms, and advanced operations. Includes practical coding examples and problem-solving techniques using trees.", 186 | "title": "Data Structures and Algorithms using Python", 187 | "category": "Data Structures", 188 | "user_profile_link": "https://github.com/SwatiSTiwari", 189 | "user_Image": "https://avatars.githubusercontent.com/SwatiSTiwari" 190 | }, 191 | { 192 | "name": "Swati Tiwari", 193 | "playlist_link": "https://www.youtube.com/watch?v=wm5gMKuwSYk&t=85s", 194 | "summary": "Comprehensive beginner's guide to Next.js framework. Covers core concepts, routing, server-side rendering, and API routes. Includes practical examples for building modern web applications with Next.js.", 195 | "title": "Nextjs course for beginners", 196 | "category": "Frontend Development", 197 | "user_profile_link": "https://github.com/SwatiSTiwari", 198 | "user_Image": "https://avatars.githubusercontent.com/SwatiSTiwari" 199 | }, 200 | { 201 | "name": "Aaditya Padte", 202 | "playlist_link": "https://www.youtube.com/playlist?list=PLgUwDviBIf0oE3gA41TKO2H5bHpPd7fzn", 203 | "summary": "Comprehensive playlist covering graph algorithms and their patterns. Includes in-depth explanations of various graph traversal and problem-solving techniques. Provides implementation examples in both C++ and Java for better understanding.", 204 | "title": "Graphs (Cpp & Java)", 205 | "category": "Data Structures", 206 | "user_profile_link": "https://github.com/Aaditya8C", 207 | "user_Image": "https://avatars.githubusercontent.com/Aaditya8C" 208 | }, 209 | { 210 | "name": "Aaditya Padte", 211 | "playlist_link": "https://www.youtube.com/playlist?list=PLgUwDviBIf0qUlt5H_kiKYaNSqJ81PMMY", 212 | "summary": "Extensive playlist on dynamic programming and its various patterns. Covers fundamental to advanced DP concepts with detailed problem-solving approaches. Provides implementation examples in both C++ and Java for comprehensive learning.", 213 | "title": "DP Series (Cpp & Java)", 214 | "category": "Data Structures", 215 | "user_profile_link": "https://github.com/Aaditya8C", 216 | "user_Image": "https://avatars.githubusercontent.com/Aaditya8C" 217 | }, 218 | { 219 | "name": "Muiz Zatam", 220 | "playlist_link": "https://youtube.com/playlist?list=PLoROMvodv4rMiGQp3WXShtMGgzqpfVfbU&si=0shh4wNGI8tkrLnG", 221 | "summary": "Stanford's CS-229 expects more than just learning from you! Taught by few of the best professors in the field such as Andrew Ng, this playlist will take the gaps in your ML Theory and will set you apart from the mainstream. With that being said, ML is a vast subject and demands a great deal of focus. You can't learn anything without a motive. This course is a theoretical way of understanding ML which is a skill that most beginners lack. So, with little to no implementation - the aim of the course is essentially to guide you concisely upon the things that happen under the hood. A personal suggestion would be to follow along with the course along with an implementational guide.", 222 | "title": "Machine Learning by Andrew Ng!", 223 | "category": "Machine Learning", 224 | "user_profile_link": "https://github.com/MuizZatam", 225 | "user_Image": "https://avatars.githubusercontent.com/MuizZatam" 226 | }, 227 | { 228 | "name": "Anas Khan", 229 | "playlist_link": "https://youtu.be/wSHwm29QJzI?si=cXqf7yj0Kn-UBSMk", 230 | "summary": "This tutorial guides you through building a full-stack SaaS app for collecting website feedback, featuring an embeddable widget and admin dashboard. It covers essential concepts like web components, authentication, database operations, TypeScript, and NextJS, ensuring a comprehensive learning experience.", 231 | "title": "Build a SaaS Application with NextJs", 232 | "category": "Projects", 233 | "user_profile_link": "https://github.com/anaskhan28", 234 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 235 | }, 236 | { 237 | "name": "Anas Khan", 238 | "playlist_link": "https://youtube.com/playlist?list=PLinedj3B30sDby4Al-i13hQJGQoRQDfPo&si=JJhgG2qb94feCXNn", 239 | "summary": "It includes working with Node.js modules, creating web servers and APIs using the Express framework, and using MongoDB for data storage. The course also covers server-side rendering with EJS, authentication and authorization with JWT tokens, advanced patterns like MVC, and building servers with Express and TypeScript, among other topics.", 240 | "title": "Master NodeJS", 241 | "category": "Backend Development", 242 | "user_profile_link": "https://github.com/anaskhan28", 243 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 244 | }, 245 | { 246 | "name": "Anas Khan", 247 | "playlist_link": "https://www.youtube.com/playlist?list=PLyzOVJj3bHQuloKGG59rS43e29ro7I57J", 248 | "summary": "This series covers crucial, often overlooked computing and software skills essential for computer science students and professionals. Topics include fundamental and advanced shell usage, data wrangling, version control with Git, text editing with vim, debugging and profiling, metaprogramming, and basics of security and cryptography.", 249 | "title": "Advanced Shell Scripting and Computing Techniques", 250 | "category": "Backend Development", 251 | "user_profile_link": "https://github.com/anaskhan28", 252 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 253 | }, 254 | { 255 | "name": "Anas Khan", 256 | "playlist_link": "https://youtu.be/cZTHGPn1jdU?si=TvWuSC1kdGw91o9T", 257 | "summary": " Deep-diving into the world of Web Crypto APIs, starting from key generation, various encryption algorithms, implementation techniques. It also teaches alot about security and how you can implement it and more.", 258 | "title": "Encrypting Data in the Browse Javascript", 259 | "category": "Backend Development", 260 | "user_profile_link": "https://github.com/anaskhan28", 261 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 262 | }, 263 | { 264 | "name": "Anas Khan", 265 | "playlist_link": "https://youtube.com/playlist?list=PLPYK3ecn6pnzOE9Ek9Ijx_RyZNh8oBs7a&si=eJIetg2DuVuakO9U", 266 | "summary": "This playlist consist of AI related projects, podcast and all the random yet impactful video topics on AI wchich can help you to learn in a better way. Also have a lot of information related to AI, AGI and various other things.", 267 | "title": "AI Stuff", 268 | "category": "Machine Learning", 269 | "user_profile_link": "https://github.com/anaskhan28", 270 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 271 | }, 272 | { 273 | "name": "Drastic Coder", 274 | "playlist_link": "https://youtube.com/playlist?list=PL3bGVFXuWmKKs_xHKrdD4nlEwy6o7r5aH", 275 | "summary": "An in-depth tutorial series on the T3 stack, focusing on building modern full-stack applications using Next.js, TypeScript, tRPC, Prisma, Tailwind CSS, and React.", 276 | "title": "T3 Stack Tutorials & Projects", 277 | "category": "Full-Stack Development", 278 | "user_profile_link": "https://github.com/DrasticCoder", 279 | "user_Image": "https://avatars.githubusercontent.com/DrasticCoder" 280 | }, 281 | { 282 | "name": "Drastic Coder", 283 | "playlist_link": "https://youtube.com/playlist?list=PLvenS7mPQE1Fybu3hFuuIqgQnxVqfGj8d", 284 | "summary": "A practical series for lazy developers exploring Fastify, focusing on building efficient and scalable backend applications with minimal effort.", 285 | "title": "Fastify for Lazy Developers: Quick and Scalable Backends", 286 | "category": "Backend Development", 287 | "user_profile_link": "https://github.com/DrasticCoder", 288 | "user_Image": "https://avatars.githubusercontent.com/DrasticCoder" 289 | }, 290 | { 291 | "name": "Drastic Coder", 292 | "playlist_link": "https://youtube.com/playlist?list=PLA-LNl4iXkpbwJ42x0Yq3n2IsHc-kpr0Z", 293 | "summary": "A detailed guide to using the ShadCN UI library, demonstrating how to build beautiful and responsive UIs with minimal code and effort. docs - https://ui.shadcn.com/", 294 | "title": "Building UIs with Shad CN library", 295 | "category": "Frontend Development", 296 | "user_profile_link": "https://github.com/DrasticCoder", 297 | "user_Image": "https://avatars.githubusercontent.com/DrasticCoder" 298 | }, 299 | { 300 | "name": "Dark-Kernel", 301 | "playlist_link": "https://www.youtube.com/watch?v=oSZI11ilWjU&list=PL1H1sBF1VAKVmjZZr162aUNCt2Uy5ozAG", 302 | "summary": "Beginner-friendly tutorial on cybercrime investigation, whcih Provides practical examples for effective management and collection of data. by john hammond", 303 | "title": "Cybercrime Investigation", 304 | "category": "Cyber Security", 305 | "user_profile_link": "https://github.com/Dark-Kernel", 306 | "user_Image": "https://avatars.githubusercontent.com/Dark-Kernel" 307 | }, 308 | { 309 | "name": "Manjiri C", 310 | "playlist_link": "https://www.youtube.com/watch?v=pN6jk0uUrD8&list=PLlasXeu85E9cQ32gLCvAvr9vNaUccPVNP", 311 | "summary": "This playlist, created by Akshay Saini, provides an in-depth and beginner-friendly course on JavaScript. It covers the core concepts of JavaScript with clear explanations and practical examples, making it a valuable resource for developers looking to strengthen their JS fundamentals.", 312 | "title": "Namaste JavaScript 🙏 by Akshay Saini", 313 | "category": "Frontend Development", 314 | "user_profile_link": "https://github.com/CODEX108", 315 | "user_Image": "https://avatars.githubusercontent.com/u/82377810?v=4" 316 | }, 317 | { 318 | "name": "Anas Khan", 319 | "playlist_link": "https://www.youtube.com/watch?v=hkKdRrKf_0o&list=PL-my9REMIFtEsQv2rezRY7E9Ih9nmJ2TZ", 320 | "summary": "This playlist, created by You suck at programming, provides an in-depth and beginner-friendly course on Bash scripting. It covers the core concepts of Bash with clear explanations and practical examples, making it a valuable resource for developers looking to strengthen their Unix shell fundamentals.", 321 | "title": "You suck at programming", 322 | "category": "Cyber Security", 323 | "user_profile_link": "https://github.com/anaskhan28", 324 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 325 | }, 326 | { 327 | "name": "Anas Khan", 328 | "playlist_link": "https://www.youtube.com/playlist?list=PLQGFK8RiEPSKsY3QbZ8zUTj8RGJ6NnvAZ", 329 | "summary": "This playlist offers an immersive learning experience, making it easy to dive into Frappe Framework, whether you're a beginner or looking to sharpen your skills.", 330 | "title": "Frappe Framework App", 331 | "category": "Full-Stack Development", 332 | "user_profile_link": "https://github.com/anaskhan28", 333 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 334 | }, 335 | { 336 | "name": "anaskhan28", 337 | "playlist_link": "https://youtube.com/playlist?list=PLBqEQje7T7jFVVDdj_NYWAeDebpUEEUo_", 338 | "summary": "\nIt teaches about renting server, hosting our stuffs on it. How to connect to server with ssh keys, setup domain for it, etc.\nHe is an Arch Linux user so you can except everything at grass level :)", 339 | "title": "Self Hosting Series", 340 | "category": "Other", 341 | "user_profile_link": "https://github.com/anaskhan28", 342 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 343 | }, 344 | { 345 | "name": "anaskhan28", 346 | "playlist_link": "https://youtu.be/amAq-WHAFs8?si=LgE8aTn-5vA0IdIn", 347 | "summary": "\nThis is the web3 video with solana and it is for beginners who wants to learn and explore web3, it is really good and the concept were explained neatly", 348 | "title": "Web3 for beginners", 349 | "category": "Frontend Development", 350 | "user_profile_link": "https://github.com/anaskhan28", 351 | "user_Image": "https://avatars.githubusercontent.com/anaskhan28" 352 | }, 353 | { 354 | "name": "ikamalagrahari", 355 | "playlist_link": "https://youtube.com/playlist?list=PLMCXHnjXnTnvo6alSjVkgxV-VH6EPyvoX&si=pMtqWuafEyJQgycs", 356 | "summary": "\nThis curated collection of videos focuses on various aspects of system design, offering insights into designing scalable and efficient systems.", 357 | "title": "System Design", 358 | "category": "System Design", 359 | "user_profile_link": "https://github.com/ikamalagrahari", 360 | "user_Image": "https://avatars.githubusercontent.com/ikamalagrahari" 361 | } 362 | ] 363 | --------------------------------------------------------------------------------