Home
Softono
a

adrianhajdin

Professional software vendor delivering innovative solutions on the Softono platform. Specialized in both open-source and proprietary software development.

Total Products
18

Software by adrianhajdin

healthcare
Open Source

healthcare

<div align="center"> <br /> <a href="https://youtu.be/lEflo_sc82g?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/healthcare/assets/151519281/a7dd73b6-93de-484d-84e0-e7f4e299167b" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> <img src="https://img.shields.io/badge/-Appwrite-black?style=for-the-badge&logoColor=white&logo=appwrite&color=FD366E" alt="appwrite" /> </div> <h3 align="center">A HealthCare Management System</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets (Code to Copy)](#snippets) 6. πŸ”— [Assets](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/lEflo_sc82g?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> A healthcare patient management application that allows patients to easily register, book, and manage their appointments with doctors, featuring administrative tools for scheduling, confirming, and canceling appointments, along with SMS notifications, all built using Next.js. If you're getting started and need assistance or face any bugs, join our active Discord community with over **34k+** members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - Appwrite - Typescript - TailwindCSS - ShadCN - Twilio ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Register as a Patient**: Users can sign up and create a personal profile as a patient. πŸ‘‰ **Book a New Appointment with Doctor**: Patients can schedule appointments with doctors at their convenience and can book multiple appointments. πŸ‘‰ **Manage Appointments on Admin Side**: Administrators can efficiently view and handle all scheduled appointments. πŸ‘‰ **Confirm/Schedule Appointment from Admin Side**: Admins can confirm and set appointment times to ensure they are properly scheduled. πŸ‘‰ **Cancel Appointment from Admin Side**: Administrators have the ability to cancel any appointment as needed. πŸ‘‰ **Send SMS on Appointment Confirmation**: Patients receive SMS notifications to confirm their appointment details. πŸ‘‰ **Complete Responsiveness**: The application works seamlessly on all device types and screen sizes. πŸ‘‰ **File Upload Using Appwrite Storage**: Users can upload and store files securely within the app using Appwrite storage services. πŸ‘‰ **Manage and Track Application Performance Using Sentry**: The application uses Sentry to monitor and track its performance and detect any errors. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/healthcare.git cd healthcare ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env.local` in the root of your project and add the following content: ```env #APPWRITE NEXT_PUBLIC_ENDPOINT=https://cloud.appwrite.io/v1 PROJECT_ID= API_KEY= DATABASE_ID= PATIENT_COLLECTION_ID= APPOINTMENT_COLLECTION_ID= NEXT_PUBLIC_BUCKET_ID= NEXT_PUBLIC_ADMIN_PASSKEY=111111 ``` Replace the placeholder values with your actual Appwrite credentials. You can obtain these credentials by signing up on the [Appwrite website](https://appwrite.io/). **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript import type { Config } from "tailwindcss"; const { fontFamily } = require("tailwindcss/defaultTheme"); const config = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { green: { 500: "#24AE7C", 600: "#0D2A1F", }, blue: { 500: "#79B5EC", 600: "#152432", }, red: { 500: "#F37877", 600: "#3E1716", 700: "#F24E43", }, light: { 200: "#E8E9E9", }, dark: { 200: "#0D0F10", 300: "#131619", 400: "#1A1D21", 500: "#363A3D", 600: "#76828D", 700: "#ABB8C4", }, }, fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], }, backgroundImage: { appointments: "url('/assets/images/appointments-bg.png')", pending: "url('/assets/images/pending-bg.png')", cancelled: "url('/assets/images/cancelled-bg.png')", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, "caret-blink": { "0%,70%,100%": { opacity: "1" }, "20%,50%": { opacity: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "caret-blink": "caret-blink 1.25s ease-out infinite", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config; export default config; ``` </details> <details> <summary><code>app/globals.css</code></summary> ```css @tailwind base; @tailwind components; @tailwind utilities; /* ========================================== TAILWIND STYLES */ @layer base { /* Remove scrollbar */ .remove-scrollbar::-webkit-scrollbar { width: 0px; height: 0px; border-radius: 0px; } .remove-scrollbar::-webkit-scrollbar-track { background: transparent; } .remove-scrollbar::-webkit-scrollbar-thumb { background: transparent; border-radius: 0px; } .remove-scrollbar::-webkit-scrollbar-thumb:hover { /* background: #1e2238; */ background: transparent; } } @layer utilities { /* ===== UTILITIES */ .sidebar { @apply remove-scrollbar w-full max-w-72 flex-col overflow-auto bg-black-800 px-7 py-10; } .left-sidebar { @apply hidden lg:flex; } .right-sidebar { @apply hidden xl:flex; } .clip-text { @apply bg-clip-text text-transparent; } .bg-image { @apply bg-black-900 bg-light-rays bg-cover bg-no-repeat; } .header { @apply text-32-bold md:text-36-bold; } .sub-header { @apply text-18-bold md:text-24-bold; } .container { @apply relative flex-1 overflow-y-auto px-[5%]; } .sub-container { @apply mx-auto flex size-full flex-col py-10; } .side-img { @apply hidden h-full object-cover md:block; } .copyright { @apply text-14-regular justify-items-end text-center text-dark-600 xl:text-left; } /* ==== SUCCESS */ .success-img { @apply m-auto flex flex-1 flex-col items-center justify-between gap-10 py-10; } .request-details { @apply flex w-full flex-col items-center gap-8 border-y-2 border-dark-400 py-8 md:w-fit md:flex-row; } /* ===== ADMIN */ .admin-header { @apply sticky top-3 z-20 mx-3 flex items-center justify-between rounded-2xl bg-dark-200 px-[5%] py-5 shadow-lg xl:px-12; } .admin-main { @apply flex flex-col items-center space-y-6 px-[5%] pb-12 xl:space-y-12 xl:px-12; } .admin-stat { @apply flex w-full flex-col justify-between gap-5 sm:flex-row xl:gap-10; } /* ==== FORM */ .radio-group { @apply flex h-full flex-1 items-center gap-2 rounded-md border border-dashed border-dark-500 bg-dark-400 p-3; } .checkbox-label { @apply cursor-pointer text-sm font-medium text-dark-700 peer-disabled:cursor-not-allowed peer-disabled:opacity-70 md:leading-none; } /* ==== File Upload */ .file-upload { @apply text-12-regular flex cursor-pointer flex-col items-center justify-center gap-3 rounded-md border border-dashed border-dark-500 bg-dark-400 p-5; } .file-upload_label { @apply flex flex-col justify-center gap-2 text-center text-dark-600; } /* ==== Stat Card */ .stat-card { @apply flex flex-1 flex-col gap-6 rounded-2xl bg-cover p-6 shadow-lg; } /* ==== Status Badge */ .status-badge { @apply flex w-fit items-center gap-2 rounded-full px-4 py-2; } /* Data Table */ .data-table { @apply z-10 w-full overflow-hidden rounded-lg border border-dark-400 shadow-lg; } .table-actions { @apply flex w-full items-center justify-between space-x-2 p-4; } /* ===== ALIGNMENTS */ .flex-center { @apply flex items-center justify-center; } .flex-between { @apply flex items-center justify-between; } /* ===== TYPOGRAPHY */ .text-36-bold { @apply text-[36px] leading-[40px] font-bold; } .text-24-bold { @apply text-[24px] leading-[28px] font-bold; } .text-32-bold { @apply text-[32px] leading-[36px] font-bold; } .text-18-bold { @apply text-[18px] leading-[24px] font-bold; } .text-16-semibold { @apply text-[16px] leading-[20px] font-semibold; } .text-16-regular { @apply text-[16px] leading-[20px] font-normal; } .text-14-medium { @apply text-[14px] leading-[18px] font-medium; } .text-14-regular { @apply text-[14px] leading-[18px] font-normal; } .text-12-regular { @apply text-[12px] leading-[16px] font-normal; } .text-12-semibold { @apply text-[12px] leading-[16px] font-semibold; } /* ===== SHADCN OVERRIDES */ .shad-primary-btn { @apply bg-green-500 text-white !important; } .shad-danger-btn { @apply bg-red-700 text-white !important; } .shad-gray-btn { @apply border border-dark-500 cursor-pointer bg-dark-400 text-white !important; } .shad-input-label { @apply text-14-medium text-dark-700 !important; } .shad-input { @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 h-11 focus-visible:ring-0 focus-visible:ring-offset-0 !important; } .shad-input-icon { @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 h-11 focus-visible:ring-0 focus-visible:ring-offset-0 !important; } .shad-textArea { @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 focus-visible:ring-0 focus-visible:ring-offset-0 !important; } .shad-combobox-item { @apply data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 !important; } .shad-combobox-trigger { @apply h-11 !important; } .shad-select-trigger { @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 h-11 focus:ring-0 focus:ring-offset-0 !important; } .shad-select-content { @apply bg-dark-400 border-dark-500 !important; } .shad-dialog { @apply bg-dark-400 border-dark-500 !important; } .shad-dialog button { @apply focus:ring-0 focus:ring-offset-0 focus-visible:border-none focus-visible:outline-none focus-visible:ring-transparent focus-visible:ring-offset-0 !important; } .shad-error { @apply text-red-400 !important; } .shad-table { @apply rounded-lg overflow-hidden !important; } .shad-table-row-header { @apply border-b border-dark-400 text-light-200 hover:bg-transparent !important; } .shad-table-row { @apply border-b border-dark-400 text-light-200 !important; } .shad-otp { @apply w-full flex justify-between !important; } .shad-otp-slot { @apply text-36-bold justify-center flex border border-dark-500 rounded-lg size-16 gap-4 !important; } .shad-alert-dialog { @apply space-y-5 bg-dark-400 border-dark-500 outline-none !important; } .shad-sheet-content button { @apply top-2 focus:ring-0 focus:ring-offset-0 focus-visible:border-none focus-visible:outline-none focus-visible:ring-transparent focus-visible:ring-offset-0 !important; } /* ===== REACT PHONE NUMBER INPUT OVERRIDES */ .input-phone { @apply mt-2 h-11 rounded-md px-3 text-sm border bg-dark-400 placeholder:text-dark-600 border-dark-500 !important; } /* ===== REACT DATE PICKER OVERRIDES */ .date-picker { @apply overflow-hidden border-transparent w-full placeholder:text-dark-600 h-11 text-14-medium rounded-md px-3 outline-none !important; } } /* ===== REACT-DATEPICKER OVERRIDES */ .react-datepicker-wrapper.date-picker { display: flex; align-items: center; } .react-datepicker, .react-datepicker__time, .react-datepicker__header, .react-datepicker__current-month, .react-datepicker__day-name, .react-datepicker__day, .react-datepicker-time__header { background-color: #1a1d21 !important; border-color: #363a3d !important; color: #abb8c4 !important; } .react-datepicker__current-month, .react-datepicker__day-name, .react-datepicker-time__header { color: #ffffff !important; } .react-datepicker__triangle { fill: #1a1d21 !important; color: #1a1d21 !important; stroke: #1a1d21 !important; } .react-datepicker__time-list-item:hover { background-color: #363a3d !important; } .react-datepicker__input-container input { background-color: #1a1d21 !important; width: 100%; outline: none; } .react-datepicker__day--selected { background-color: #24ae7c !important; color: #ffffff !important; border-radius: 4px; } .react-datepicker__time-list-item--selected { background-color: #24ae7c !important; } .react-datepicker__time-container { border-left: 1px solid #363a3d !important; } .react-datepicker__time-list-item { display: flex !important; align-items: center !important; } /* ===== REACT PHONE NUMBER INPUT OVERRIDES */ .PhoneInputInput { outline: none; margin-left: 4px; background: #1a1d21; font-size: 14px; font-weight: 500; } .PhoneInputInput::placeholder { color: #1a1d21; } ``` </details> <details> <summary><code>types/index.d.ts</code></summary> ```typescript /* eslint-disable no-unused-vars */ declare type SearchParamProps = { params: { [key: string]: string }; searchParams: { [key: string]: string | string[] | undefined }; }; declare type Gender = "Male" | "Female" | "Other"; declare type Status = "pending" | "scheduled" | "cancelled"; declare interface CreateUserParams { name: string; email: string; phone: string; } declare interface User extends CreateUserParams { $id: string; } declare interface RegisterUserParams extends CreateUserParams { userId: string; birthDate: Date; gender: Gender; address: string; occupation: string; emergencyContactName: string; emergencyContactNumber: string; primaryPhysician: string; insuranceProvider: string; insurancePolicyNumber: string; allergies: string | undefined; currentMedication: string | undefined; familyMedicalHistory: string | undefined; pastMedicalHistory: string | undefined; identificationType: string | undefined; identificationNumber: string | undefined; identificationDocument: FormData | undefined; privacyConsent: boolean; } declare type CreateAppointmentParams = { userId: string; patient: string; primaryPhysician: string; reason: string; schedule: Date; status: Status; note: string | undefined; }; declare type UpdateAppointmentParams = { appointmentId: string; userId: string; appointment: Appointment; type: string; }; ``` </details> <details> <summary><code>types/appwrite.types.ts</code></summary> ```typescript import { Models } from "node-appwrite"; export interface Patient extends Models.Document { userId: string; name: string; email: string; phone: string; birthDate: Date; gender: Gender; address: string; occupation: string; emergencyContactName: string; emergencyContactNumber: string; primaryPhysician: string; insuranceProvider: string; insurancePolicyNumber: string; allergies: string | undefined; currentMedication: string | undefined; familyMedicalHistory: string | undefined; pastMedicalHistory: string | undefined; identificationType: string | undefined; identificationNumber: string | undefined; identificationDocument: FormData | undefined; privacyConsent: boolean; } export interface Appointment extends Models.Document { patient: Patient; schedule: Date; status: Status; primaryPhysician: string; reason: string; note: string; userId: string; cancellationReason: string | null; } ``` </details> <details> <summary><code>lib/utils.ts</code></summary> ```typescript import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value)); export const convertFileToUrl = (file: File) => URL.createObjectURL(file); // FORMAT DATE TIME export const formatDateTime = (dateString: Date | string) => { const dateTimeOptions: Intl.DateTimeFormatOptions = { // weekday: "short", // abbreviated weekday name (e.g., 'Mon') month: "short", // abbreviated month name (e.g., 'Oct') day: "numeric", // numeric day of the month (e.g., '25') year: "numeric", // numeric year (e.g., '2023') hour: "numeric", // numeric hour (e.g., '8') minute: "numeric", // numeric minute (e.g., '30') hour12: true, // use 12-hour clock (true) or 24-hour clock (false) }; const dateDayOptions: Intl.DateTimeFormatOptions = { weekday: "short", // abbreviated weekday name (e.g., 'Mon') year: "numeric", // numeric year (e.g., '2023') month: "2-digit", // abbreviated month name (e.g., 'Oct') day: "2-digit", // numeric day of the month (e.g., '25') }; const dateOptions: Intl.DateTimeFormatOptions = { month: "short", // abbreviated month name (e.g., 'Oct') year: "numeric", // numeric year (e.g., '2023') day: "numeric", // numeric day of the month (e.g., '25') }; const timeOptions: Intl.DateTimeFormatOptions = { hour: "numeric", // numeric hour (e.g., '8') minute: "numeric", // numeric minute (e.g., '30') hour12: true, // use 12-hour clock (true) or 24-hour clock (false) }; const formattedDateTime: string = new Date(dateString).toLocaleString( "en-US", dateTimeOptions ); const formattedDateDay: string = new Date(dateString).toLocaleString( "en-US", dateDayOptions ); const formattedDate: string = new Date(dateString).toLocaleString( "en-US", dateOptions ); const formattedTime: string = new Date(dateString).toLocaleString( "en-US", timeOptions ); return { dateTime: formattedDateTime, dateDay: formattedDateDay, dateOnly: formattedDate, timeOnly: formattedTime, }; }; export function encryptKey(passkey: string) { return btoa(passkey); } export function decryptKey(passkey: string) { return atob(passkey); } ``` </details> <details> <summary><code>lib/validation.ts</code></summary> ```typescript import { z } from "zod"; export const UserFormValidation = z.object({ name: z .string() .min(2, "Name must be at least 2 characters") .max(50, "Name must be at most 50 characters"), email: z.string().email("Invalid email address"), phone: z .string() .refine((phone) => /^\+\d{10,15}$/.test(phone), "Invalid phone number"), }); export const PatientFormValidation = z.object({ name: z .string() .min(2, "Name must be at least 2 characters") .max(50, "Name must be at most 50 characters"), email: z.string().email("Invalid email address"), phone: z .string() .refine((phone) => /^\+\d{10,15}$/.test(phone), "Invalid phone number"), birthDate: z.coerce.date(), gender: z.enum(["Male", "Female", "Other"]), address: z .string() .min(5, "Address must be at least 5 characters") .max(500, "Address must be at most 500 characters"), occupation: z .string() .min(2, "Occupation must be at least 2 characters") .max(500, "Occupation must be at most 500 characters"), emergencyContactName: z .string() .min(2, "Contact name must be at least 2 characters") .max(50, "Contact name must be at most 50 characters"), emergencyContactNumber: z .string() .refine( (emergencyContactNumber) => /^\+\d{10,15}$/.test(emergencyContactNumber), "Invalid phone number" ), primaryPhysician: z.string().min(2, "Select at least one doctor"), insuranceProvider: z .string() .min(2, "Insurance name must be at least 2 characters") .max(50, "Insurance name must be at most 50 characters"), insurancePolicyNumber: z .string() .min(2, "Policy number must be at least 2 characters") .max(50, "Policy number must be at most 50 characters"), allergies: z.string().optional(), currentMedication: z.string().optional(), familyMedicalHistory: z.string().optional(), pastMedicalHistory: z.string().optional(), identificationType: z.string().optional(), identificationNumber: z.string().optional(), identificationDocument: z.custom<File[]>().optional(), treatmentConsent: z .boolean() .default(false) .refine((value) => value === true, { message: "You must consent to treatment in order to proceed", }), disclosureConsent: z .boolean() .default(false) .refine((value) => value === true, { message: "You must consent to disclosure in order to proceed", }), privacyConsent: z .boolean() .default(false) .refine((value) => value === true, { message: "You must consent to privacy in order to proceed", }), }); export const CreateAppointmentSchema = z.object({ primaryPhysician: z.string().min(2, "Select at least one doctor"), schedule: z.coerce.date(), reason: z .string() .min(2, "Reason must be at least 2 characters") .max(500, "Reason must be at most 500 characters"), note: z.string().optional(), cancellationReason: z.string().optional(), }); export const ScheduleAppointmentSchema = z.object({ primaryPhysician: z.string().min(2, "Select at least one doctor"), schedule: z.coerce.date(), reason: z.string().optional(), note: z.string().optional(), cancellationReason: z.string().optional(), }); export const CancelAppointmentSchema = z.object({ primaryPhysician: z.string().min(2, "Select at least one doctor"), schedule: z.coerce.date(), reason: z.string().optional(), note: z.string().optional(), cancellationReason: z .string() .min(2, "Reason must be at least 2 characters") .max(500, "Reason must be at most 500 characters"), }); export function getAppointmentSchema(type: string) { switch (type) { case "create": return CreateAppointmentSchema; case "cancel": return CancelAppointmentSchema; default: return ScheduleAppointmentSchema; } } ``` </details> <details> <summary><code>constants/index.ts</code></summary> ```typescript export const GenderOptions = ["Male", "Female", "Other"]; export const PatientFormDefaultValues = { firstName: "", lastName: "", email: "", phone: "", birthDate: new Date(Date.now()), gender: "Male" as Gender, address: "", occupation: "", emergencyContactName: "", emergencyContactNumber: "", primaryPhysician: "", insuranceProvider: "", insurancePolicyNumber: "", allergies: "", currentMedication: "", familyMedicalHistory: "", pastMedicalHistory: "", identificationType: "Birth Certificate", identificationNumber: "", identificationDocument: [], treatmentConsent: false, disclosureConsent: false, privacyConsent: false, }; export const IdentificationTypes = [ "Birth Certificate", "Driver's License", "Medical Insurance Card/Policy", "Military ID Card", "National Identity Card", "Passport", "Resident Alien Card (Green Card)", "Social Security Card", "State ID Card", "Student ID Card", "Voter ID Card", ]; export const Doctors = [ { image: "/assets/images/dr-green.png", name: "John Green", }, { image: "/assets/images/dr-cameron.png", name: "Leila Cameron", }, { image: "/assets/images/dr-livingston.png", name: "David Livingston", }, { image: "/assets/images/dr-peter.png", name: "Evan Peter", }, { image: "/assets/images/dr-powell.png", name: "Jane Powell", }, { image: "/assets/images/dr-remirez.png", name: "Alex Ramirez", }, { image: "/assets/images/dr-lee.png", name: "Jasmine Lee", }, { image: "/assets/images/dr-cruz.png", name: "Alyana Cruz", }, { image: "/assets/images/dr-sharma.png", name: "Hardik Sharma", }, ]; export const StatusIcon = { scheduled: "/assets/icons/check.svg", pending: "/assets/icons/pending.svg", cancelled: "/assets/icons/cancelled.svg", }; ``` </details> ## <a name="links">πŸ”— Assets</a> Public assets used in the project can be found [here](https://drive.google.com/file/d/1yGvWFeSaH1_-aiQ1gejT23lqz5979RKB/view?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Frontend Templates Medical & Health Records
2.7K Github Stars
project_hoobank
Open Source

project_hoobank

# HooBank - Modern UI/UX website using React.js & Tailwind CSS ![HooBank](https://i.ibb.co/BK1Hn0x/Screenshot-2022-08-08-at-4-05-48-PM.png) ### [🌟 Become a top 1% Next.js 13 developer in only one course](https://jsmastery.pro/next13) ### [πŸš€ Land your dream programming job in 6 months](https://jsmastery.pro/masterclass) ### Showcase your dev skills with practical experience and land the coding career of your dreams πŸ’» JS Mastery Pro - https://jsmastery.pro/youtube βœ… A special YOUTUBE discount code is automatically applied! πŸ“™ Get the Ultimate Frontend & Backend Development Roadmaps, a Complete JavaScript Cheatsheet, Portfolio Tips, and more - https://www.jsmastery.pro/links

Frontend Templates
2.6K Github Stars
brainwave
Open Source

brainwave

<div align="center"> <br /> <a href="https://youtu.be/B91wc5dCEBA" target="_blank"> <img src="https://i.ibb.co/Kqdv8j1/Image-from.png" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Vite-black?style=for-the-badge&logoColor=white&logo=vite&color=646CFF" alt="vite" /> <img src="https://img.shields.io/badge/-React_JS-black?style=for-the-badge&logoColor=white&logo=react&color=61DAFB" alt="react.js" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">Modern UI/UX website</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/B91wc5dCEBA" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Brainwave - Modern UI/UX website, developed using React.js and Tailwind CSS, exemplifies modern UI/UX principles. Its sleek design, seamless animations, and overall user experience set a high standard, serving as a reference or inspiration for future modern applications or websites in general. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Vite - React.js - Tailwind CSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Beautiful Sections**: Includes hero, services, features, how to use, roadmap, pricing, footer, and header. πŸ‘‰ **Parallax Animations**: Engaging effects triggered by mouse movement and scrolling πŸ‘‰ **Complex UI Geometry**: Utilizes tailwindcss for intricate shapes like circular feature displays, grid lines, and side lines. πŸ‘‰ **Latest UI Trends**: Incorporates modern design elements such as bento grids. πŸ‘‰ **Cool Gradients**: Enhances visuals with stylish gradients using Tailwind CSS for cards, buttons, etc. πŸ‘‰ **Responsive**: Ensures seamless functionality and aesthetics across all devices and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/JavaScript-Mastery-Pro/brainwave.git cd brainwave ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Running the Project** ```bash npm run dev ``` Open [http://localhost:5173](http://localhost:5173) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>.vscode/settings.json</code></summary> ```json { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.addMissingImports": "explicit" }, "prettier.tabWidth": 2, "prettier.useTabs": false, "prettier.semi": true, "prettier.singleQuote": false, "prettier.jsxSingleQuote": false, "prettier.trailingComma": "es5", "prettier.arrowParens": "always", "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[css]": { "editor.defaultFormatter": "vscode.css-language-features" }, "[svg]": { "editor.defaultFormatter": "jock.svg" } } ``` </details> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ import { fontFamily } from "tailwindcss/defaultTheme"; import plugin from "tailwindcss/plugin"; export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./public/assets/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { colors: { color: { 1: "#AC6AFF", 2: "#FFC876", 3: "#FF776F", 4: "#7ADB78", 5: "#858DFF", 6: "#FF98E2", }, stroke: { 1: "#26242C", }, n: { 1: "#FFFFFF", 2: "#CAC6DD", 3: "#ADA8C3", 4: "#757185", 5: "#3F3A52", 6: "#252134", 7: "#15131D", 8: "#0E0C15", 9: "#474060", 10: "#43435C", 11: "#1B1B2E", 12: "#2E2A41", 13: "#6C7275", }, }, fontFamily: { sans: ["var(--font-sora)", ...fontFamily.sans], code: "var(--font-code)", grotesk: "var(--font-grotesk)", }, letterSpacing: { tagline: ".15em", }, spacing: { 0.25: "0.0625rem", 7.5: "1.875rem", 15: "3.75rem", }, opacity: { 15: ".15", }, transitionDuration: { DEFAULT: "200ms", }, transitionTimingFunction: { DEFAULT: "linear", }, zIndex: { 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", }, borderWidth: { DEFAULT: "0.0625rem", }, backgroundImage: { "radial-gradient": "radial-gradient(var(--tw-gradient-stops))", "conic-gradient": "conic-gradient(from 225deg, #FFC876, #79FFF7, #9F53FF, #FF98E2, #FFC876)", "benefit-card-1": "url(assets/benefits/card-1.svg)", "benefit-card-2": "url(assets/benefits/card-2.svg)", "benefit-card-3": "url(assets/benefits/card-3.svg)", "benefit-card-4": "url(assets/benefits/card-4.svg)", "benefit-card-5": "url(assets/benefits/card-5.svg)", "benefit-card-6": "url(assets/benefits/card-6.svg)", }, }, }, plugins: [ plugin(function ({ addBase, addComponents, addUtilities }) { addBase({}); addComponents({ ".container": { "@apply max-w-[77.5rem] mx-auto px-5 md:px-10 lg:px-15 xl:max-w-[87.5rem]": {}, }, ".h1": { "@apply font-semibold text-[2.5rem] leading-[3.25rem] md:text-[2.75rem] md:leading-[3.75rem] lg:text-[3.25rem] lg:leading-[4.0625rem] xl:text-[3.75rem] xl:leading-[4.5rem]": {}, }, ".h2": { "@apply text-[1.75rem] leading-[2.5rem] md:text-[2rem] md:leading-[2.5rem] lg:text-[2.5rem] lg:leading-[3.5rem] xl:text-[3rem] xl:leading-tight": {}, }, ".h3": { "@apply text-[2rem] leading-normal md:text-[2.5rem]": {}, }, ".h4": { "@apply text-[2rem] leading-normal": {}, }, ".h5": { "@apply text-2xl leading-normal": {}, }, ".h6": { "@apply font-semibold text-lg leading-8": {}, }, ".body-1": { "@apply text-[0.875rem] leading-[1.5rem] md:text-[1rem] md:leading-[1.75rem] lg:text-[1.25rem] lg:leading-8": {}, }, ".body-2": { "@apply font-light text-[0.875rem] leading-6 md:text-base": {}, }, ".caption": { "@apply text-sm": {}, }, ".tagline": { "@apply font-grotesk font-light text-xs tracking-tagline uppercase": {}, }, ".quote": { "@apply font-code text-lg leading-normal": {}, }, ".button": { "@apply font-code text-xs font-bold uppercase tracking-wider": {}, }, }); addUtilities({ ".tap-highlight-color": { "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)", }, }); }), ], }; ``` </details> <details> <summary><code>index.css</code></summary> ```css @import url("https://fonts.googleapis.com/css2?family=Sora:wght@300;400;600&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;600;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; :root { --font-sora: "Sora", sans-serif; --font-code: "Source Code Pro", monospace; --font-grotesk: "Space Grotesk", sans-serif; } * { scroll-behavior: smooth; } @layer base { body { @apply font-sans bg-n-8 text-n-1 text-base; } } .rotate-45 { @apply rotate-[45deg]; } .rotate-90 { @apply rotate-[90deg]; } .rotate-135 { @apply rotate-[135deg]; } .rotate-180 { @apply rotate-[180deg]; } .rotate-225 { @apply rotate-[225deg]; } .rotate-270 { @apply rotate-[270deg]; } .rotate-315 { @apply rotate-[315deg]; } .rotate-360 { @apply rotate-[360deg]; } .-rotate-45 { @apply rotate-[-45deg]; } .-rotate-90 { @apply rotate-[-90deg]; } .-rotate-135 { @apply rotate-[-135deg]; } .-rotate-180 { @apply rotate-[-180deg]; } .-rotate-225 { @apply rotate-[-225deg]; } .-rotate-270 { @apply rotate-[-270deg]; } .-rotate-315 { @apply rotate-[-315deg]; } .-rotate-360 { @apply rotate-[-360deg]; } ``` </details> <details> <summary><code>constants/index.js</code></summary> ```javascript import { benefitIcon1, benefitIcon2, benefitIcon3, benefitIcon4, benefitImage2, chromecast, disc02, discord, discordBlack, facebook, figma, file02, framer, homeSmile, instagram, notification2, notification3, notification4, notion, photoshop, plusSquare, protopie, raindrop, recording01, recording03, roadmap1, roadmap2, roadmap3, roadmap4, searchMd, slack, sliders04, telegram, twitter, yourlogo, } from "../../public/assets"; export const navigation = [ { id: "0", title: "Features", url: "#features", }, { id: "1", title: "Pricing", url: "#pricing", }, { id: "2", title: "How to use", url: "#how-to-use", }, { id: "3", title: "Roadmap", url: "#roadmap", }, { id: "4", title: "New account", url: "#signup", onlyMobile: true, }, { id: "5", title: "Sign in", url: "#login", onlyMobile: true, }, ]; export const heroIcons = [homeSmile, file02, searchMd, plusSquare]; export const notificationImages = [notification4, notification3, notification2]; export const companyLogos = [yourlogo, yourlogo, yourlogo, yourlogo, yourlogo]; export const brainwaveServices = [ "Photo generating", "Photo enhance", "Seamless Integration", ]; export const brainwaveServicesIcons = [ recording03, recording01, disc02, chromecast, sliders04, ]; export const roadmap = [ { id: "0", title: "Voice recognition", text: "Enable the chatbot to understand and respond to voice commands, making it easier for users to interact with the app hands-free.", date: "May 2023", status: "done", imageUrl: roadmap1, colorful: true, }, { id: "1", title: "Gamification", text: "Add game-like elements, such as badges or leaderboards, to incentivize users to engage with the chatbot more frequently.", date: "May 2023", status: "progress", imageUrl: roadmap2, }, { id: "2", title: "Chatbot customization", text: "Allow users to customize the chatbot's appearance and behavior, making it more engaging and fun to interact with.", date: "May 2023", status: "done", imageUrl: roadmap3, }, { id: "3", title: "Integration with APIs", text: "Allow the chatbot to access external data sources, such as weather APIs or news APIs, to provide more relevant recommendations.", date: "May 2023", status: "progress", imageUrl: roadmap4, }, ]; export const collabText = "With smart automation and top-notch security, it's the perfect solution for teams looking to work smarter."; export const collabContent = [ { id: "0", title: "Seamless Integration", text: collabText, }, { id: "1", title: "Smart Automation", }, { id: "2", title: "Top-notch Security", }, ]; export const collabApps = [ { id: "0", title: "Figma", icon: figma, width: 26, height: 36, }, { id: "1", title: "Notion", icon: notion, width: 34, height: 36, }, { id: "2", title: "Discord", icon: discord, width: 36, height: 28, }, { id: "3", title: "Slack", icon: slack, width: 34, height: 35, }, { id: "4", title: "Photoshop", icon: photoshop, width: 34, height: 34, }, { id: "5", title: "Protopie", icon: protopie, width: 34, height: 34, }, { id: "6", title: "Framer", icon: framer, width: 26, height: 34, }, { id: "7", title: "Raindrop", icon: raindrop, width: 38, height: 32, }, ]; export const pricing = [ { id: "0", title: "Basic", description: "AI chatbot, personalized recommendations", price: "0", features: [ "An AI chatbot that can understand your queries", "Personalized recommendations based on your preferences", "Ability to explore the app and its features without any cost", ], }, { id: "1", title: "Premium", description: "Advanced AI chatbot, priority support, analytics dashboard", price: "9.99", features: [ "An advanced AI chatbot that can understand complex queries", "An analytics dashboard to track your conversations", "Priority support to solve issues quickly", ], }, { id: "2", title: "Enterprise", description: "Custom AI chatbot, advanced analytics, dedicated account", price: null, features: [ "An AI chatbot that can understand your queries", "Personalized recommendations based on your preferences", "Ability to explore the app and its features without any cost", ], }, ]; export const benefits = [ { id: "0", title: "Ask anything", text: "Lets users quickly find answers to their questions without having to search through multiple sources.", backgroundUrl: "assets/benefits/card-1.svg", iconUrl: benefitIcon1, imageUrl: benefitImage2, }, { id: "1", title: "Improve everyday", text: "The app uses natural language processing to understand user queries and provide accurate and relevant responses.", backgroundUrl: "assets/benefits/card-2.svg", iconUrl: benefitIcon2, imageUrl: benefitImage2, light: true, }, { id: "2", title: "Connect everywhere", text: "Connect with the AI chatbot from anywhere, on any device, making it more accessible and convenient.", backgroundUrl: "assets/benefits/card-3.svg", iconUrl: benefitIcon3, imageUrl: benefitImage2, }, { id: "3", title: "Fast responding", text: "Lets users quickly find answers to their questions without having to search through multiple sources.", backgroundUrl: "assets/benefits/card-4.svg", iconUrl: benefitIcon4, imageUrl: benefitImage2, light: true, }, { id: "4", title: "Ask anything", text: "Lets users quickly find answers to their questions without having to search through multiple sources.", backgroundUrl: "assets/benefits/card-5.svg", iconUrl: benefitIcon1, imageUrl: benefitImage2, }, { id: "5", title: "Improve everyday", text: "The app uses natural language processing to understand user queries and provide accurate and relevant responses.", backgroundUrl: "assets/benefits/card-6.svg", iconUrl: benefitIcon2, imageUrl: benefitImage2, }, ]; export const socials = [ { id: "0", title: "Discord", iconUrl: discordBlack, url: "#", }, { id: "1", title: "Twitter", iconUrl: twitter, url: "#", }, { id: "2", title: "Instagram", iconUrl: instagram, url: "#", }, { id: "3", title: "Telegram", iconUrl: telegram, url: "#", }, { id: "4", title: "Facebook", iconUrl: facebook, url: "#", }, ]; ``` </details> <details> <summary><code>components/Section.jsx</code></summary> ```javascript import SectionSvg from "../../public/assets/svg/SectionSvg"; const Section = ({ className, id, crosses, crossesOffset, customPaddings, children, }) => ( <div id={id} className={`relative ${ customPaddings || `py-10 lg:py-16 xl:py-20 ${crosses ? "lg:py-32 xl:py-40" : ""}` } ${className || ""}`} > {children} <div className="hidden absolute top-0 left-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:left-7.5 xl:left-10" /> <div className="hidden absolute top-0 right-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:right-7.5 xl:right-10" /> {crosses && ( <> <div className={`hidden absolute top-0 left-7.5 right-7.5 h-0.25 bg-stroke-1 ${ crossesOffset && crossesOffset } pointer-events-none lg:block xl:left-10 right-10`} /> <SectionSvg crossesOffset={crossesOffset} /> </> )} </div> ); export default Section; ``` </details> <details> <summary><code>components/Roadmap.jsx</code></summary> ```javascript import Button from "./Button"; import Heading from "./Heading"; import Section from "./Section"; import Tagline from "./TagLine"; import { roadmap } from "../constants"; import { check2, grid, loading1 } from "../../public/assets"; import { Gradient } from "./design/Roadmap"; const Roadmap = () => ( <Section className="overflow-hidden" id="roadmap"> <div className="container md:pb-10"> <Heading tag="Ready to get started" title="What we’re working on" /> <div className="relative grid gap-6 md:grid-cols-2 md:gap-4 md:pb-[7rem]"> {roadmap.map((item) => { const status = item.status === "done" ? "Done" : "In progress"; return ( <div className={`md:flex even:md:translate-y-[7rem] p-0.25 rounded-[2.5rem] ${ item.colorful ? "bg-conic-gradient" : "bg-n-6" }`} key={item.id} > <div className="relative p-8 bg-n-8 rounded-[2.4375rem] overflow-hidden xl:p-15"> <div className="absolute top-0 left-0 max-w-full"> <img className="w-full" src={grid} width={550} height={550} alt="Grid" /> </div> <div className="relative z-1"> <div className="flex items-center justify-between max-w-[27rem] mb-8 md:mb-20"> <Tagline>{item.date}</Tagline> <div className="flex items-center px-4 py-1 bg-n-1 rounded text-n-8"> <img className="mr-2.5" src={item.status === "done" ? check2 : loading1} width={16} height={16} alt={status} /> <div className="tagline">{status}</div> </div> </div> <div className="mb-10 -my-10 -mx-15"> <img className="w-full" src={item.imageUrl} width={628} height={426} alt={item.title} /> </div> <h4 className="h4 mb-4">{item.title}</h4> <p className="body-2 text-n-4">{item.text}</p> </div> </div> </div> ); })} <Gradient /> </div> <div className="flex justify-center mt-12 md:mt-15 xl:mt-20"> <Button href="/roadmap">Our roadmap</Button> </div> </div> </Section> ); export default Roadmap; ``` </details> ## <a name="links">πŸ”— Links</a> - [Assets](https://drive.google.com/file/d/1JKzwPl_hnpjIlNbwfjMagb4HosxnyXbf/view?usp=sharing) - [Design](https://drive.google.com/file/d/15WJMOchujvaQ7Kg9e0nGeGR7G7JOeX1K/view?usp=sharing) - [Absolute Relative Positioning](https://css-tricks.com/absolute-positioning-inside-relative-positioning/) - [Live Website](https://jsm-brainwave.com/) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning experience. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://www.jsmastery.pro/ultimate-next-course" target="_blank"> <img src="https://i.ibb.co/804sPK6/Image-720.png" alt="Project Banner"> </a>

Web Development Education & Learning
1.9K Github Stars
nike_landing_page
Open Source

nike_landing_page

<div align="center"> <br /> <a href="https://youtu.be/tS7upsfuxmo?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/nike_landing_page/assets/151519281/36013f49-ba13-47ad-a6c4-f9d58bfae7fc" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">TailwindCSS Crash Course</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/tS7upsfuxmo?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Master Tailwind CSS in two parts by first learning fundamentals, advanced techniques, and theming. Then, build a stunning Nike landing page, applying learned skills to create a visually impressive website. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Tailwind CSS - React.js ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Maximizing Tailwind CSS**: Discover tips and tricks to make the most out of Tailwind CSS. πŸ‘‰ **Understanding Tailwind Internals**: Dive into the inner workings of Tailwind, gaining insights into its structure and optimizations. πŸ‘‰ **Best Practices**: Learn Tailwind's best practices for efficient and maintainable code. πŸ‘‰ **Theming**:Explore techniques to add different themes to your website using Tailwind CSS. πŸ‘‰ **JavaScript-like Tasks with Tailwind**: Discover how Tailwind CSS can be used to achieve tasks that typically require JavaScript code while building a beautiful Nike Website with a, πŸ‘‰ **Complex Hero Section**: A visually appealing hero section showcasing key elements. πŸ‘‰ **Popular Products Showcase**: A section highlighting popular Nike products πŸ‘‰ **About Us Section**: An informative "About Us" section with a unique design. πŸ‘‰ **Special Offers**: Showcase special offers in an eye-catching manner πŸ‘‰ **Testimonials**: A testimonials section for a captivating user experience πŸ‘‰ **Newsletter Integration**: A newsletter section with Tailwind styling, encouraging user engagement πŸ‘‰ **Footer**: A comprehensive footer section containing various links πŸ‘‰ **Mobile Responsive**: The entire website is responsive across various devices, emphasizing Tailwind's mobile-friendly capabilities. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/nike_landing_page.git cd nike_landing_page ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Running the Project** ```bash npm start ``` Open [http://localhost:5173](http://localhost:5173) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>.eslintrc.cjs</code></summary> ```javascript module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, settings: { react: { version: '18.2' } }, plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], "react/prop-types": 0 }, } ``` </details> <details> <summary><code>constants.index.js</code></summary> ```javascript import { facebook, instagram, shieldTick, support, truckFast, twitter } from "../assets/icons"; import { bigShoe1, bigShoe2, bigShoe3, customer1, customer2, shoe4, shoe5, shoe6, shoe7, thumbnailShoe1, thumbnailShoe2, thumbnailShoe3 } from "../assets/images"; export const navLinks = [ { href: "#home", label: "Home" }, { href: "#about-us", label: "About Us" }, { href: "#products", label: "Products" }, { href: "#contact-us", label: "Contact Us" }, ]; export const shoes = [ { thumbnail: thumbnailShoe1, bigShoe: bigShoe1, }, { thumbnail: thumbnailShoe2, bigShoe: bigShoe2, }, { thumbnail: thumbnailShoe3, bigShoe: bigShoe3, }, ]; export const statistics = [ { value: '1k+', label: 'Brands' }, { value: '500+', label: 'Shops' }, { value: '250k+', label: 'Customers' }, ]; export const products = [ { imgURL: shoe4, name: "Nike Air Jordan-01", price: "$200.20", }, { imgURL: shoe5, name: "Nike Air Jordan-10", price: "$210.20", }, { imgURL: shoe6, name: "Nike Air Jordan-100", price: "$220.20", }, { imgURL: shoe7, name: "Nike Air Jordan-001", price: "$230.20", }, ]; export const services = [ { imgURL: truckFast, label: "Free shipping", subtext: "Enjoy seamless shopping with our complimentary shipping service." }, { imgURL: shieldTick, label: "Secure Payment", subtext: "Experience worry-free transactions with our secure payment options." }, { imgURL: support, label: "Love to help you", subtext: "Our dedicated team is here to assist you every step of the way." }, ]; export const reviews = [ { imgURL: customer1, customerName: 'Morich Brown', rating: 4.5, feedback: "The attention to detail and the quality of the product exceeded my expectations. Highly recommended!" }, { imgURL: customer2, customerName: 'Lota Mongeskar', rating: 4.5, feedback: "The product not only met but exceeded my expectations. I'll definitely be a returning customer!" } ]; export const footerLinks = [ { title: "Products", links: [ { name: "Air Force 1", link: "/" }, { name: "Air Max 1", link: "/" }, { name: "Air Jordan 1", link: "/" }, { name: "Air Force 2", link: "/" }, { name: "Nike Waffle Racer", link: "/" }, { name: "Nike Cortez", link: "/" }, ], }, { title: "Help", links: [ { name: "About us", link: "/" }, { name: "FAQs", link: "/" }, { name: "How it works", link: "/" }, { name: "Privacy policy", link: "/" }, { name: "Payment policy", link: "/" }, ], }, { title: "Get in touch", links: [ { name: "[email protected]", link: "mailto:[email protected]" }, { name: "+92554862354", link: "tel:+92554862354" }, ], }, ]; export const socialMedia = [ { src: facebook, alt: "facebook logo" }, { src: twitter, alt: "twitter logo" }, { src: instagram, alt: "instagram logo" }, ]; ``` </details> <details> <summary><code>index.css</code></summary> ```css @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&family=Palanquin:wght@100;200;300;400;500;600;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Palanquin:wght@100;200;300;400;500;600;700&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; } @layer components { .max-container { max-width: 1440px; margin: 0 auto; } .input { @apply sm:flex-1 max-sm:w-full text-base leading-normal text-slate-gray pl-5 max-sm:p-5 outline-none sm:border-none border max-sm:border-slate-gray max-sm:rounded-full; } } @layer utilities { .padding { @apply sm:px-16 px-8 sm:py-24 py-12; } .padding-x { @apply sm:px-16 px-8; } .padding-y { @apply sm:py-24 py-12; } .padding-l { @apply sm:pl-16 pl-8; } .padding-r { @apply sm:pr-16 pr-8; } .padding-t { @apply sm:pt-24 pt-12; } .padding-b { @apply sm:pb-24 pb-12; } .info-text { @apply font-montserrat text-slate-gray text-lg leading-7; } } ``` </details> <details> <summary><code>script.js</code></summary> ```javascript // To showcase the demo of dark theme. Copy paste :) <script type="text/javascript"> document.addEventListener("DOMContentLoaded", () => { const toggleDark = document.getElementById('toggleDark') toggleDark.addEventListener('click', function() { if(document.documentElement.classList.includes('dark')) { document.documentElement.classList.remove('dark') } else { document.documentElement.classList.add('dark') } alert("click!") }); }); </script> ``` </details> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { fontSize: { xs: ['12px', '16px'], sm: ['14px', '20px'], base: ['16px', '19.5px'], lg: ['18px', '21.94px'], xl: ['20px', '24.38px'], '2xl': ['24px', '29.26px'], '3xl': ['28px', '50px'], '4xl': ['48px', '58px'], '8xl': ['96px', '106px'] }, extend: { fontFamily: { palanquin: ['Palanquin', 'sans-serif'], montserrat: ['Montserrat', 'sans-serif'], }, colors: { 'primary': "#ECEEFF", "coral-red": "#FF6452", "slate-gray": "#6D6D6D", "pale-blue": "#F5F6FF", "white-400": "rgba(255, 255, 255, 0.80)" }, boxShadow: { '3xl': '0 10px 40px rgba(0, 0, 0, 0.1)' }, backgroundImage: { 'hero': "url('assets/images/collection-background.svg')", 'card': "url('assets/images/thumbnail-background.svg')", }, screens: { "wide": "1440px" } }, }, plugins: [], } ``` </details> ## <a name="links">πŸ”— Links</a> - Assets used in the project are [here](https://drive.google.com/file/d/1ccqjc8gJ7CLvXT_vUhVT4Gmys-Ze13FK/view) - [Tailwind Play](https://play.tailwindcss.com/) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Frontend Templates
1.5K Github Stars
3D_portfolio
Open Source

3D_portfolio

# Build and Deploy an Amazing 3D Developer Portfolio in React with Three.js ![3D Website](https://i.ibb.co/ryytGVx/Screenshot-2023-11-25-at-11-28-11-AM.png) ### [🌟 Become a top 1% Next.js 14 developer in only one course](https://jsmastery.pro/next14) ### [πŸš€ Land your dream programming job in 6 months](https://jsmastery.pro/masterclass) ### [πŸ“™ Free Three.js Cheatsheet](https://resource.jsmastery.pro/threejs-cheatsheet) ### [🌐 Best Hosting for Your Websites](https://hostinger.com/javascript10)

Design & Creative 3D Modeling & Animation
1.3K Github Stars
project_music_player
Open Source

project_music_player

# Build and Deploy a Better Spotify 2.0 Clone Music App with React 18! (Tailwind, Shazam, Redux) ![Spotify Clone](https://i.ibb.co/mFh2kGZ/Thumbnail-2.png) ### [🌟 Become a top 1% Next.js 13 developer in only one course](https://jsmastery.pro/next13) ### [πŸš€ Land your dream programming job in 6 months](https://jsmastery.pro/masterclass) ### Launch your development career with project-based coaching on [JS Mastery Pro](https://www.jsmastery.pro).

Frontend Templates Music Streaming
1.1K Github Stars
travel_ui_ux
Open Source

travel_ui_ux

<div align="center"> <br /> <a href="https://youtu.be/cuzw4vL1z5E?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/travel_ui_ux/assets/151519281/c72ebf75-847d-4a98-8f8a-24be02dac752" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Typescript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">Travel Website</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/cuzw4vL1z5E?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Built with Next.js, TailwindCSS, and TypeScript, this landing page not only looks great but also serves as a hands-on project to reinforce one's understanding of these technologies. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - Tailwind CSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Appealing Hero Section**: Visually striking hero section that captures attention and sets the tone for the website πŸ‘‰ **Camp Exploration Section**: Showcase various camps in a dedicated section, allowing users to explore different options. πŸ‘‰ **Engaging Travel Guide Section**: A compelling travel guide section with informative content and captivating visuals. πŸ‘‰ **Feature-Rich Section**: A complex and feature-rich section offering advanced functionalities or in-depth information. πŸ‘‰ **Call to Action for Mobile Apps**: A call-to-action section encouraging users to explore the mobile app version, complete with download links. πŸ‘‰ **Footer**: Comprehensive footer section containing site and social media links for easy navigation and connectivity. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/travel_ui_ux.git cd travel_ui_ux ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>constants.index.ts</code></summary> ```typescript // NAVIGATION export const NAV_LINKS = [ { href: '/', key: 'home', label: 'Home' }, { href: '/', key: 'how_hilink_work', label: 'How Hilink Work?' }, { href: '/', key: 'services', label: 'Services' }, { href: '/', key: 'pricing ', label: 'Pricing ' }, { href: '/', key: 'contact_us', label: 'Contact Us' }, ]; // CAMP SECTION export const PEOPLE_URL = [ '/person-1.png', '/person-2.png', '/person-3.png', '/person-4.png', ]; // FEATURES SECTION export const FEATURES = [ { title: 'Real maps can be offline', icon: '/map.svg', variant: 'green', description: 'We provide a solution for you to be able to use our application when climbing, yes offline maps you can use at any time there is no signal at the location', }, { title: 'Set an adventure schedule', icon: '/calendar.svg', variant: 'green', description: "Schedule an adventure with friends. On holidays, there are many interesting offers from Hilink. That way, there's no more discussion", }, { title: 'Technology using augment reality', icon: '/tech.svg', variant: 'green', description: 'Technology uses augmented reality as a guide to your hiking trail in the forest to the top of the mountain. Already supported by the latest technology without an internet connection', }, { title: 'Many new locations every month', icon: '/location.svg', variant: 'orange', description: 'Lots of new locations every month, because we have a worldwide community of climbers who share their best experiences with climbing', }, ]; // FOOTER SECTION export const FOOTER_LINKS = [ { title: 'Learn More', links: [ 'About Hilink', 'Press Releases', 'Environment', 'Jobs', 'Privacy Policy', 'Contact Us', ], }, { title: 'Our Community', links: ['Climbing xixixi', 'Hiking hilink', 'Hilink kinthill'], }, ]; export const FOOTER_CONTACT_INFO = { title: 'Contact Us', links: [ { label: 'Admin Officer', value: '123-456-7890' }, { label: 'Email Officer', value: '[email protected]' }, ], }; export const SOCIALS = { title: 'Social', links: [ '/facebook.svg', '/instagram.svg', '/twitter.svg', '/youtube.svg', '/wordpress.svg', ], }; ``` </details> <details> <summary><code>globals.css</code></summary> ```typescript @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Inter; } @layer utilities { .btn_white { @apply border-white bg-white px-8 py-3 text-green-50 } .btn_white_text { @apply border-white bg-white px-8 py-3 text-gray-90 } .btn_green { @apply border-green-50 bg-green-50 px-8 py-5 text-white } .btn_dark_green { @apply bg-green-90 px-8 py-4 text-white transition-all hover:bg-black } .btn_dark_green_outline { @apply border-gray-20 bg-green-90 px-8 py-5 text-white } .max-container { @apply mx-auto max-w-[1440px]; } .padding-container { @apply px-6 lg:px-20 3xl:px-0; } .flexCenter { @apply flex items-center justify-center; } .flexBetween { @apply flex items-center justify-between; } .flexStart { @apply flex items-center justify-start; } .flexEnd { @apply flex items-center justify-end; } /* FONTS */ .regular-64 { @apply text-[64px] font-[400] leading-[120%]; } .regular-40 { @apply text-[40px] font-[400] leading-[120%]; } .regular-32 { @apply text-[32px] font-[400]; } .regular-24 { @apply text-[24px] font-[400]; } .regular-20 { @apply text-[20px] font-[400]; } .regular-18 { @apply text-[18px] font-[400]; } .regular-16 { @apply text-[16px] font-[400]; } .regular-14 { @apply text-[14px] font-[400]; } .medium-14 { @apply text-[14px] font-[600]; } .bold-88 { @apply text-[88px] font-[700] leading-[120%]; } .bold-64 { @apply text-[64px] font-[700] leading-[120%]; } .bold-52 { @apply text-[52px] font-[700] leading-[120%]; } .bold-40 { @apply text-[40px] font-[700] leading-[120%]; } .bold-32 { @apply text-[32px] font-[700] leading-[120%]; } .bold-20 { @apply text-[20px] font-[700]; } .bold-18 { @apply text-[18px] font-[700]; } .bold-16 { @apply text-[16px] font-[700]; } /* Hero */ .hero-map { @apply absolute right-0 top-0 h-screen w-screen bg-pattern-2 bg-cover bg-center md:-right-28 xl:-top-60; } /* Camp */ .camp-quote { @apply absolute -right-6 bottom-4 w-[140px] lg:bottom-10 xl:-right-8 xl:w-[186px] 3xl:right-0; } /* Feature */ .feature-phone { @apply absolute top-[13%] z-10 hidden max-w-[1500px] rotate-[15deg] md:-left-16 lg:flex 3xl:left-20; } /* Get App */ .get-app { @apply max-container relative flex w-full flex-col justify-between gap-32 overflow-hidden bg-green-90 bg-pattern bg-cover bg-center bg-no-repeat px-6 py-12 text-white sm:flex-row sm:gap-12 sm:py-24 lg:px-20 xl:max-h-[598px] 2xl:rounded-5xl; } } /* Hide scrollbar for Chrome, Safari and Opera */ .hide-scrollbar::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE, Edge and Firefox */ .hide-scrollbar { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } ``` </details> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { green: { 50: '#30AF5B', 90: '#292C27', }, gray: { 10: '#EEEEEE', 20: '#A2A2A2', 30: '#7B7B7B', 50: '#585858', 90: '#141414', }, orange: { 50: '#FF814C', }, blue: { 70: '#021639', }, yellow: { 50: '#FEC601', }, }, backgroundImage: { 'bg-img-1': "url('/img-1.png')", 'bg-img-2': "url('/img-2.png')", 'feature-bg': "url('/feature-bg.png')", pattern: "url('/pattern.png')", 'pattern-2': "url('/pattern-bg.png')", }, screens: { xs: '400px', '3xl': '1680px', '4xl': '2200px', }, maxWidth: { '10xl': '1512px', }, borderRadius: { '5xl': '40px', }, }, }, plugins: [], }; ``` </details> ## <a name="links">πŸ”— Links</a> Assets used in the project are here [here](https://drive.google.com/file/d/10bwdMeLAl7scTjrorqtG3v2Z6b4b7S-w/view?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Frontend Templates UI Design & Prototyping
869 Github Stars
portfolio
Open Source

portfolio

WordPress Theme Development. Here is the collection of the coding and boiler plate for the Portfolio.

Web Development Productivity Frontend Templates
14 Github Stars
project_next_14_ai_prompt_sharing
Open Source

project_next_14_ai_prompt_sharing

<div align="center"> <br /> <a href="https://youtu.be/wm5gMKuwSYk?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/project_next_14_ai_prompt_sharing/assets/151519281/40a6e6fe-7f97-45bc-8113-bb5732fdcdb7" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000" alt="Next.js" /> <img src="https://img.shields.io/badge/-Mongodb-black?style=for-the-badge&logoColor=white&logo=mongodb&color=47A248" alt="mongodb" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">Next.js 14 AI Prompt Sharing Application</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/wm5gMKuwSYk?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Develop a Next.js application that highlights the key features of Next.js along with a comprehensive CRUD AI Prompt sharing system utilizing a MongoDB database and implementing NextAuth authentication. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - MongoDB - NextAuth - TailwindCSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Modern Design with Glassmorphism Trend Style**: A modern and visually appealing design, incorporating the glassmorphism trend style for a sleek and contemporary appearance. πŸ‘‰ **Discover and Share AI Prompts**: Allow users to discover AI prompts shared by the community and create their own prompts to share with the world. πŸ‘‰ **Edit and Delete Created Prompts**: Users have the ability to edit their created prompts at any time and delete them when needed. πŸ‘‰ **Profile Page**: Each user gets a dedicated profile page showcasing all the prompts they've created, providing an overview of their contributions. πŸ‘‰ **View Other People's Profiles**: Users can explore the profiles of other creators to view the prompts they've shared, fostering a sense of community. πŸ‘‰ **Copy to Clipboard**: Implement a convenient "Copy to Clipboard" functionality for users to easily copy the AI prompts for their use. πŸ‘‰ **Search Prompts by Specific Tag**: Allow users to search for prompts based on specific tags, making it easier to find prompts related to specific topics. πŸ‘‰ **Google Authentication using NextAuth**: Enable secure Google authentication using NextAuth, ensuring a streamlined and trustworthy login experience. πŸ‘‰ **Responsive Website**: Develop a fully responsive website to ensure optimal user experience across various devices, from desktops to smartphones and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/project_next_13_ai_prompt_sharing.git cd project_next_13_ai_prompt_sharing ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env` in the root of your project and add the following content: ```env NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL_INTERNAL=http://localhost:3000 NEXTAUTH_SECRET= GOOGLE_ID= GOOGLE_CLIENT_SECRET= MONGODB_URI= ``` Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up on these corresponding websites from [Google Cloud Console](https://console.cloud.google.com/welcome?rapt=AEjHL4MBaLLneW6OfAHf_zgms1eWZFw1wdy0_KIC4uh1nEqh2m4ojOvrXNlzJ4h7CZTkpiWgcsoHbUvS-FMdCP7WIkaVlPAeU7cnVR6Y0wJHeLMOtU6KAzA&project=promptopia-385410), [Cryptpool](https://www.cryptool.org/en/cto/openssl) (for random Auth Secret), and [MongoDB](https://www.mongodb.com/). **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>globals.css</code></summary> ```css @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; /* Note: The styles for this gradient grid background is heavily inspired by the creator of this amazing site (https://dub.sh) – all credits go to them! */ .main { width: 100vw; min-height: 100vh; position: fixed; display: flex; justify-content: center; padding: 120px 24px 160px 24px; pointer-events: none; } .main:before { background: radial-gradient(circle, rgba(2, 0, 36, 0) 0, #fafafa 100%); position: absolute; content: ""; z-index: 2; width: 100%; height: 100%; top: 0; } .main:after { content: ""; background-image: url("/assets/images/grid.svg"); z-index: 1; position: absolute; width: 100%; height: 100%; top: 0; opacity: 0.4; filter: invert(1); } .gradient { height: fit-content; z-index: 3; width: 100%; max-width: 640px; background-image: radial-gradient( at 27% 37%, hsla(215, 98%, 61%, 1) 0px, transparent 0% ), radial-gradient(at 97% 21%, hsla(125, 98%, 72%, 1) 0px, transparent 50%), radial-gradient(at 52% 99%, hsla(354, 98%, 61%, 1) 0px, transparent 50%), radial-gradient(at 10% 29%, hsla(256, 96%, 67%, 1) 0px, transparent 50%), radial-gradient(at 97% 96%, hsla(38, 60%, 74%, 1) 0px, transparent 50%), radial-gradient(at 33% 50%, hsla(222, 67%, 73%, 1) 0px, transparent 50%), radial-gradient(at 79% 53%, hsla(343, 68%, 79%, 1) 0px, transparent 50%); position: absolute; content: ""; width: 100%; height: 100%; filter: blur(100px) saturate(150%); top: 80px; opacity: 0.15; } @media screen and (max-width: 640px) { .main { padding: 0; } } /* Tailwind Styles */ .app { @apply relative z-10 flex justify-center items-center flex-col max-w-7xl mx-auto sm:px-16 px-6; } .black_btn { @apply rounded-full border border-black bg-black py-1.5 px-5 text-white transition-all hover:bg-white hover:text-black text-center text-sm font-inter flex items-center justify-center; } .outline_btn { @apply rounded-full border border-black bg-transparent py-1.5 px-5 text-black transition-all hover:bg-black hover:text-white text-center text-sm font-inter flex items-center justify-center; } .head_text { @apply mt-5 text-5xl font-extrabold leading-[1.15] text-black sm:text-6xl; } .orange_gradient { @apply bg-gradient-to-r from-amber-500 via-orange-600 to-yellow-500 bg-clip-text text-transparent; } .green_gradient { @apply bg-gradient-to-r from-green-400 to-green-500 bg-clip-text text-transparent; } .blue_gradient { @apply bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent; } .desc { @apply mt-5 text-lg text-gray-600 sm:text-xl max-w-2xl; } .search_input { @apply block w-full rounded-md border border-gray-200 bg-white py-2.5 font-satoshi pl-5 pr-12 text-sm shadow-lg font-medium focus:border-black focus:outline-none focus:ring-0; } .copy_btn { @apply w-7 h-7 rounded-full bg-white/10 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur flex justify-center items-center cursor-pointer; } .glassmorphism { @apply rounded-xl border border-gray-200 bg-white/20 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur p-5; } .prompt_layout { @apply space-y-6 py-8 sm:columns-2 sm:gap-6 xl:columns-3; } /* Feed Component */ .feed { @apply mt-16 mx-auto w-full max-w-xl flex justify-center items-center flex-col gap-2; } /* Form Component */ .form_textarea { @apply w-full flex rounded-lg h-[200px] mt-2 p-3 text-sm text-gray-500 outline-0; } .form_input { @apply w-full flex rounded-lg mt-2 p-3 text-sm text-gray-500 outline-0; } /* Nav Component */ .logo_text { @apply max-sm:hidden font-satoshi font-semibold text-lg text-black tracking-wide; } .dropdown { @apply absolute right-0 top-full mt-3 w-full p-5 rounded-lg bg-white min-w-[210px] flex flex-col gap-2 justify-end items-end; } .dropdown_link { @apply text-sm font-inter text-gray-700 hover:text-gray-500 font-medium; } /* PromptCard Component */ .prompt_card { @apply flex-1 break-inside-avoid rounded-lg border border-gray-300 bg-white/20 bg-clip-padding p-6 pb-4 backdrop-blur-lg backdrop-filter md:w-[360px] w-full h-fit; } .flex-center { @apply flex justify-center items-center; } .flex-start { @apply flex justify-start items-start; } .flex-end { @apply flex justify-end items-center; } .flex-between { @apply flex justify-between items-center; } ``` </details> <details> <summary><code>jsconfig.json</code></summary> ```json { "compilerOptions": { "paths": { "@*": ["./*"] } } } ``` </details> <details> <summary><code>route.js</code></summary> ```javascript import Prompt from "@models/prompt"; import { connectToDB } from "@utils/database"; export const GET = async (request, { params }) => { try { await connectToDB() const prompt = await Prompt.findById(params.id).populate("creator") if (!prompt) return new Response("Prompt Not Found", { status: 404 }); return new Response(JSON.stringify(prompt), { status: 200 }) } catch (error) { return new Response("Internal Server Error", { status: 500 }); } } export const PATCH = async (request, { params }) => { const { prompt, tag } = await request.json(); try { await connectToDB(); // Find the existing prompt by ID const existingPrompt = await Prompt.findById(params.id); if (!existingPrompt) { return new Response("Prompt not found", { status: 404 }); } // Update the prompt with new data existingPrompt.prompt = prompt; existingPrompt.tag = tag; await existingPrompt.save(); return new Response("Successfully updated the Prompts", { status: 200 }); } catch (error) { return new Response("Error Updating Prompt", { status: 500 }); } }; export const DELETE = async (request, { params }) => { try { await connectToDB(); // Find the prompt by ID and remove it await Prompt.findByIdAndRemove(params.id); return new Response("Prompt deleted successfully", { status: 200 }); } catch (error) { return new Response("Error deleting prompt", { status: 500 }); } }; ``` </details> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { fontFamily: { satoshi: ['Satoshi', 'sans-serif'], inter: ['Inter', 'sans-serif'], }, colors: { 'primary-orange': '#FF5722', } }, }, plugins: [], } ``` </details> <details> <summary><code>user.js</code></summary> ```javascript username: { type: String, required: [true, 'Username is required!'], match: [/^(?=.{8,20}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(?<![_.])$/, "Username invalid, it should contain 8-20 alphanumeric letters and be unique!"] }, ``` </details> ## <a name="links">πŸ”— Links</a> Assets used in the project can be found [here](https://drive.google.com/file/d/15bGW9HBImu1p3HAYalnaj2Ig_Sn-1c-f/view) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Education & Learning Frontend Templates
3K Github Stars
ecommerce_sanity_stripe
Open Source

ecommerce_sanity_stripe

Modern Full Stack ECommerce Application with Stripe & Sanity ![eCommerce](https://user-images.githubusercontent.com/70088342/160780701-7bb38a57-76bd-49a2-a4ec-49f89c50a7c7.png) ## Launch your development career with project-based coaching - https://www.jsmastery.pro **Build and Deploy a fully responsive Modern Full Stack Ecommerce application with Payments functionality**. With Modern design, animations, the ability to add and edit products on the go using a CMS, all advanced cart functionalities, and most importantly the complete integration with Stripe so that you can cover REAL payments. This is the best e-commerce website project that you can currently find on YouTube! In this video, you'll learn: - Advanced React Best Practices such as - Folder and file structure, hooks and refs - Advanced State Management of the entire application using React Context API - Next.js Best Practices such as - File-based routing, Data fetching that allows server-side rendering and static generation which makes your websites incredibly optimized (show getServerSideProps, getStaticPaths, getStaticProps), and you’ll also learn how to use Next.js as a backend endpoint. - You’ll learn how to integrate Stripe to manage payments, products, shipping rates, and the entire checkout process - And most importantly you’ll learn how to manage the entire content of your app using Sanity. Sanity is the unified content platform that’ll make the making of our entire app possible. <show sanity desk> - Through Sanity, you or your clients will be able to change the store’s homepage and more importantly, the details of all the products in the store, instantly and on the go! - Sanity allows us to focus on developing the application without having to worry about the content, file storage, and databases. They’ll cover the dirty work for us and allow us to build scalable and modern e-commerce web applications extremely easily. ![image](https://user-images.githubusercontent.com/70088342/160780701-7bb38a57-76bd-49a2-a4ec-49f89c50a7c7.png) ![image](https://user-images.githubusercontent.com/70088342/160780206-9cfe7c0a-3d8e-4a20-a055-b12efebe6c30.png) ![image](https://user-images.githubusercontent.com/70088342/160780265-692d37ac-7209-4d53-957a-e94b37d123c0.png) ![image](https://user-images.githubusercontent.com/70088342/160780381-7c947640-422e-4729-abae-21911e9bc716.png) ![image](https://user-images.githubusercontent.com/70088342/160780549-111ed048-cd4b-4740-b2fd-2c6fc3520c52.png) ![image](https://user-images.githubusercontent.com/70088342/160780884-22d6025e-9b7d-4493-8136-b3dfbf00a32f.png)

E-commerce Platforms Payment & Checkout
2.3K Github Stars
project_next13_car_showcase
Open Source

project_next13_car_showcase

<div align="center"> <br /> <a href="https://youtu.be/pUNSHPyVryU?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/project_next13_car_showcase/assets/151519281/2453c186-0ae9-448f-b3c4-077bf910680e" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">A Car Showcase Website</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/pUNSHPyVryU?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Developed with Next.js and leveraging its server-side rendering capabilities, the Car Showcase website presents various car types, showcasing comprehensive information in a well-designed format with advanced filtering and pagination support for an enhanced user experience. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - TypeScript - Tailwind CSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Home Page**: Showcases a visually appealing display of cars fetched from a third-party API, providing a captivating introduction to the diverse range of vehicles available. πŸ‘‰ **Exploration and Filtering**: Explore a wide variety of cars from around the world, utilizing a search and filter system based on criteria such as model, manufacturer, year, fuel type, and make. πŸ‘‰ **Transition to Server-Side Rendering**: A seamless transition from client-side rendering to server-side rendering, enhancing performance and providing a smoother browsing experience. πŸ‘‰ **Pagination**: For easy navigation through a large dataset of cars, allowing users to explore multiple pages effortlessly. πŸ‘‰ **Metadata Optimization and SEO**: Optimize metadata for car listing, enhancing search engine optimization (SEO) and ensuring better visibility on search engine results pages. πŸ‘‰ **TypeScript Types**: Utilize TypeScript to provide robust typing for enhanced code quality and better development πŸ‘‰ **Responsive Website Design**: The website is designed to be visually pleasing and responsive, ensuring an optimal user experience across various devices. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/project_next13_car_showcase.git cd project_next13_car_showcase ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env` in the root of your project and add the following content: ```env NEXT_PUBLIC_RAPID_API_KEY= NEXT_PUBLIC_IMAGIN_API_KEY=hrjavascript-mastery ``` Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up on the corresponding websites from [Rapid API](https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbmI1TlE1NHFGZ1JLdHU3dnAxSTU5a2R5UUM4QXxBQ3Jtc0tsUDY0aW8xMFhUZVdxMUNzSUlKUExRTG5UaDZoR3hWVFprN2tJV0k2dnk4MXo2NVFMVkk0NWhGS19Nd0g5cGRfN2JjcTdaSlJJRHJKYzlfT3lSS1M4TDVNVTV5Wl91c1lIR2VPZUYzbHJ2Tll2QkJ0aw&q=https%3A%2F%2Frapidapi.com%2Fapininjas%2Fapi%2Fcars-by-api-ninjas%3Futm_source%3Dyoutube.com%2FJavaScriptMastery%26utm_medium%3Dreferral%26utm_campaign%3DDevRel&v=pUNSHPyVryU) to [Imagin Cars](https://www.imagin.studio/solutions/api) **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>constants.ts</code></summary> ```typescript export const manufacturers = [ "Acura", "Alfa Romeo", "Aston Martin", "Audi", "Bentley", "BMW", "Buick", "Cadillac", "Chevrolet", "Chrysler", "Citroen", "Dodge", "Ferrari", "Fiat", "Ford", "GMC", "Honda", "Hyundai", "Infiniti", "Jaguar", "Jeep", "Kia", "Lamborghini", "Land Rover", "Lexus", "Lincoln", "Maserati", "Mazda", "McLaren", "Mercedes-Benz", "MINI", "Mitsubishi", "Nissan", "Porsche", "Ram", "Rolls-Royce", "Subaru", "Tesla", "Toyota", "Volkswagen", "Volvo", ]; export const yearsOfProduction = [ { title: "Year", value: "" }, { title: "2015", value: "2015" }, { title: "2016", value: "2016" }, { title: "2017", value: "2017" }, { title: "2018", value: "2018" }, { title: "2019", value: "2019" }, { title: "2020", value: "2020" }, { title: "2021", value: "2021" }, { title: "2022", value: "2022" }, { title: "2023", value: "2023" }, ]; export const fuels = [ { title: "Fuel", value: "", }, { title: "Gas", value: "Gas", }, { title: "Electricity", value: "Electricity", }, ]; export const footerLinks = [ { title: "About", links: [ { title: "How it works", url: "/" }, { title: "Featured", url: "/" }, { title: "Partnership", url: "/" }, { title: "Bussiness Relation", url: "/" }, ], }, { title: "Company", links: [ { title: "Events", url: "/" }, { title: "Blog", url: "/" }, { title: "Podcast", url: "/" }, { title: "Invite a friend", url: "/" }, ], }, { title: "Socials", links: [ { title: "Discord", url: "/" }, { title: "Instagram", url: "/" }, { title: "Twitter", url: "/" }, { title: "Facebook", url: "/" }, ], }, ]; ``` </details> <details> <summary><code>globals.css</code></summary> ```css @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Manrope", sans-serif; } /* START: General styles */ .max-width { @apply max-w-[1440px] mx-auto; } .padding-x { @apply sm:px-16 px-6; } .padding-y { @apply py-4; } .flex-center { @apply flex items-center justify-center; } .flex-between { @apply flex justify-between items-center; } .custom-btn { @apply flex flex-row relative justify-center items-center py-3 px-6 outline-none; } /* END: General styles */ /* START: Hero styles */ .hero { @apply flex xl:flex-row flex-col gap-5 relative z-0 max-w-[1440px] mx-auto; } .hero__title { @apply 2xl:text-[72px] sm:text-[64px] text-[50px] font-extrabold; } .hero__subtitle { @apply text-[27px] text-black-100 font-light mt-5; } .hero__image-container { @apply xl:flex-[1.5] flex justify-end items-end w-full xl:h-screen; } .hero__image { @apply relative xl:w-full w-[90%] xl:h-full h-[590px] z-0; } .hero__image-overlay { @apply absolute xl:-top-24 xl:-right-1/2 -right-1/4 bg-hero-bg bg-repeat-round -z-10 w-full xl:h-screen h-[590px] overflow-hidden; } /* END: Hero styles */ /* START: Home styles */ .home__text-container { @apply flex flex-col items-start justify-start gap-y-2.5 text-black-100; } .home__filters { @apply mt-12 w-full flex-between items-center flex-wrap gap-5; } .home__filter-container { @apply flex justify-start flex-wrap items-center gap-2; } .home__cars-wrapper { @apply grid 2xl:grid-cols-4 xl:grid-cols-3 md:grid-cols-2 grid-cols-1 w-full gap-8 pt-14; } .home__error-container { @apply mt-16 flex justify-center items-center flex-col gap-2; } /* END: Home styles */ /* START: Car Card styles */ .car-card { @apply flex flex-col p-6 justify-center items-start text-black-100 bg-primary-blue-100 hover:bg-white hover:shadow-md rounded-3xl; } .car-card__content { @apply w-full flex justify-between items-start gap-2; } .car-card__content-title { @apply text-[22px] leading-[26px] font-bold capitalize; } .car-card__price { @apply flex mt-6 text-[32px] leading-[38px] font-extrabold; } .car-card__price-dollar { @apply self-start text-[14px] leading-[17px] font-semibold; } .car-card__price-day { @apply self-end text-[14px] leading-[17px] font-medium; } .car-card__image { @apply relative w-full h-40 my-3 object-contain; } .car-card__icon-container { @apply flex group-hover:invisible w-full justify-between text-grey; } .car-card__icon { @apply flex flex-col justify-center items-center gap-2; } .car-card__icon-text { @apply text-[14px] leading-[17px]; } .car-card__btn-container { @apply hidden group-hover:flex absolute bottom-0 w-full z-10; } /* END: Car Card styles */ /* START: Car Details styles */ .car-details__dialog-panel { @apply relative w-full max-w-lg max-h-[90vh] overflow-y-auto transform rounded-2xl bg-white p-6 text-left shadow-xl transition-all flex flex-col gap-5; } .car-details__close-btn { @apply absolute top-2 right-2 z-10 w-fit p-2 bg-primary-blue-100 rounded-full; } .car-details__main-image { @apply relative w-full h-40 bg-pattern bg-cover bg-center rounded-lg; } /* END: Car Details styles */ /* START: Custom Filter styles */ .custom-filter__btn { @apply relative w-full min-w-[127px] flex justify-between items-center cursor-default rounded-lg bg-white py-2 px-3 text-left shadow-md sm:text-sm border; } .custom-filter__options { @apply absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm; } /* END: Custom Filter styles */ /* START: Footer styles */ .footer { @apply flex flex-col text-black-100 mt-5 border-t border-gray-100; } .footer__links-container { @apply flex max-md:flex-col flex-wrap justify-between gap-5 sm:px-16 px-6 py-10; } .footer__rights { @apply flex flex-col justify-start items-start gap-6; } .footer__links { @apply flex-1 w-full flex md:justify-end flex-wrap max-md:mt-10 gap-20; } .footer__link { @apply flex flex-col gap-6 text-base min-w-[170px]; } .footer__copyrights { @apply flex justify-between items-center flex-wrap mt-10 border-t border-gray-100 sm:px-16 px-6 py-10; } .footer__copyrights-link { @apply flex-1 flex sm:justify-end justify-center max-sm:mt-4 gap-10; } /* END: Footer styles */ /* START: searchbar styles */ .searchbar { @apply flex items-center justify-start max-sm:flex-col w-full relative max-sm:gap-4 max-w-3xl; } .searchbar__item { @apply flex-1 max-sm:w-full flex justify-start items-center relative; } .searchbar__input { @apply w-full h-[48px] pl-12 p-4 bg-light-white rounded-r-full max-sm:rounded-full outline-none cursor-pointer text-sm; } /* END: searchbar styles */ /* START: search manufacturer styles */ .search-manufacturer { @apply flex-1 max-sm:w-full flex justify-start items-center; } .search-manufacturer__input { @apply w-full h-[48px] pl-12 p-4 rounded-l-full max-sm:rounded-full bg-light-white outline-none cursor-pointer text-sm; } .search-manufacturer__options { @apply absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm; } .search-manufacturer__option { @apply cursor-default select-none py-2 pl-10 pr-4; } /* END: search manufacturer styles */ ``` </details> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], mode: "jit", theme: { extend: { fontFamily: { inter: ["Inter", "sans-serif"], }, colors: { "black-100": "#2B2C35", "primary-blue": { DEFAULT: "#2B59FF", 100: "#F5F8FF", }, "secondary-orange": "#f79761", "light-white": { DEFAULT: "rgba(59,60,152,0.03)", 100: "rgba(59,60,152,0.02)", }, grey: "#747A88", }, backgroundImage: { 'pattern': "url('/pattern.png')", 'hero-bg': "url('/hero-bg.png')" } }, }, plugins: [], }; ``` </details> ## <a name="links">πŸ”— Links</a> Assets used in the project are [here](https://drive.google.com/file/d/1Ague8aTHA6JSrzy3kscEZmrJQdtDxqwy/view) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Web Development Frontend Templates
1.9K Github Stars
threads
Open Source

threads

<div align="center"> <br /> <a href="https://youtu.be/O5cmLDVTgAs?feature=shared" target="_blank"> <img src="https://github.com/adrianhajdin/threads/assets/151519281/a9cd1088-968b-4b1d-b21a-f5f97d0c202b" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-MongoDB-black?style=for-the-badge&logoColor=white&logo=mongodb&color=47A248" alt="mongodb" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> <img src="https://img.shields.io/badge/-Clerk-black?style=for-the-badge&logoColor=white&logo=clerk&color=6C47FF" alt="clerk" /> <img src="https://img.shields.io/badge/-Shadcn_UI-black?style=for-the-badge&logoColor=white&logo=shadcnui&color=000000" alt="shadcnui" /> <img src="https://img.shields.io/badge/-Zod-black?style=for-the-badge&logoColor=white&logo=zod&color=3E67B1" alt="zod" /> <img src="https://img.shields.io/badge/-Typescript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> </div> <h3 align="center">A full stack Threads Clone</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/O5cmLDVTgAs?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Build a full stack Threads clone using Next.js 14+ with a redesigned look transformed from a Figma design, user interaction to community management, technical implementation, and various features, including nested deep comments, notifications, real-time-search, and more. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - MongoDB - Shadcn UI - TailwindCSS - Clerk - Webhooks - Serverless APIs - React Hook Form - Zod - TypeScript ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Authentication**: Authentication using Clerk for email, password, and social logins (Google and GitHub) with a comprehensive profile management system. πŸ‘‰ **Visually Appealing Home Page**: A visually appealing home page showcasing the latest threads for an engaging user experience. πŸ‘‰ **Create Thread Page**: A dedicated page for users to create threads, fostering community engagement πŸ‘‰ **Commenting Feature**: A commenting feature to facilitate discussions within threads. πŸ‘‰ **Nested Commenting**: Commenting system with nested threads, providing a structured conversation flow. πŸ‘‰ **User Search with Pagination**: A user search feature with pagination for easy exploration and discovery of other users. πŸ‘‰ **Activity Page**: Display notifications on the activity page when someone comments on a user's thread, enhancing user engagement. πŸ‘‰ **Profile Page**: User profile pages for showcasing information and enabling modification of profile settings. πŸ‘‰ **Create and Invite to Communities**: Allow users to create new communities and invite others using customizable template emails. πŸ‘‰ **Community Member Management**: A user-friendly interface to manage community members, allowing role changes and removals. πŸ‘‰ **Admin-Specific Community Threads**: Enable admins to create threads specifically for their community. πŸ‘‰ **Community Search with Pagination**: A community search feature with pagination for exploring different communities. πŸ‘‰ **Community Profiles**: Display community profiles showcasing threads and members for a comprehensive overview. πŸ‘‰ **Figma Design Implementation**: Transform Figma designs into a fully functional application with pixel-perfect and responsive design. πŸ‘‰ **Blazing-Fast Performance**: Optimal performance and instantaneous page switching for a seamless user experience. πŸ‘‰ **Server Side Rendering**: Utilize Next.js with Server Side Rendering for enhanced performance and SEO benefits. πŸ‘‰ **MongoDB with Complex Schemas**: Handle complex schemas and multiple data populations using MongoDB. πŸ‘‰ **File Uploads with UploadThing**: File uploads using UploadThing for a seamless media sharing experience. πŸ‘‰ **Real-Time Events Listening**: Real-time events listening with webhooks to keep users updated. πŸ‘‰ **Middleware, API Actions, and Authorization**: Utilize middleware, API actions, and authorization for robust application security. πŸ‘‰ **Next.js Layout Route Groups**: New Next.js layout route groups for efficient routing πŸ‘‰ **Data Validation with Zod**: Data integrity with data validation using Zod πŸ‘‰ **Form Management with React Hook Form**: Efficient management of forms with React Hook Form for a streamlined user input experience. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/threads.git cd threads ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env` in the root of your project and add the following content: ```env MONGODB_URL= CLERK_SECRET_KEY= UPLOADTHING_SECRET= UPLOADTHING_APP_ID= NEXT_CLERK_WEBHOOK_SECRET= NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= ``` Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up for the corresponding websites on [MongoDB](https://www.mongodb.com/), [Clerk](https://clerk.com/), and [Uploadthing](https://uploadthing.com/). **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>clerk.route.ts</code></summary> ```typescript /* eslint-disable camelcase */ // Resource: https://clerk.com/docs/users/sync-data-to-your-backend // Above article shows why we need webhooks i.e., to sync data to our backend // Resource: https://docs.svix.com/receiving/verifying-payloads/why // It's a good practice to verify webhooks. Above article shows why we should do it import { Webhook, WebhookRequiredHeaders } from "svix"; import { headers } from "next/headers"; import { IncomingHttpHeaders } from "http"; import { NextResponse } from "next/server"; import { addMemberToCommunity, createCommunity, deleteCommunity, removeUserFromCommunity, updateCommunityInfo, } from "@/lib/actions/community.actions"; // Resource: https://clerk.com/docs/integration/webhooks#supported-events // Above document lists the supported events type EventType = | "organization.created" | "organizationInvitation.created" | "organizationMembership.created" | "organizationMembership.deleted" | "organization.updated" | "organization.deleted"; type Event = { data: Record<string, string | number | Record<string, string>[]>; object: "event"; type: EventType; }; export const POST = async (request: Request) => { const payload = await request.json(); const header = headers(); const heads = { "svix-id": header.get("svix-id"), "svix-timestamp": header.get("svix-timestamp"), "svix-signature": header.get("svix-signature"), }; // Activitate Webhook in the Clerk Dashboard. // After adding the endpoint, you'll see the secret on the right side. const wh = new Webhook(process.env.NEXT_CLERK_WEBHOOK_SECRET || ""); let evnt: Event | null = null; try { evnt = wh.verify( JSON.stringify(payload), heads as IncomingHttpHeaders & WebhookRequiredHeaders ) as Event; } catch (err) { return NextResponse.json({ message: err }, { status: 400 }); } const eventType: EventType = evnt?.type!; // Listen organization creation event if (eventType === "organization.created") { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/CreateOrganization // Show what evnt?.data sends from above resource const { id, name, slug, logo_url, image_url, created_by } = evnt?.data ?? {}; try { // @ts-ignore await createCommunity( // @ts-ignore id, name, slug, logo_url || image_url, "org bio", created_by ); return NextResponse.json({ message: "User created" }, { status: 201 }); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } // Listen organization invitation creation event. // Just to show. You can avoid this or tell people that we can create a new mongoose action and // add pending invites in the database. if (eventType === "organizationInvitation.created") { try { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Invitations#operation/CreateOrganizationInvitation console.log("Invitation created", evnt?.data); return NextResponse.json( { message: "Invitation created" }, { status: 201 } ); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } // Listen organization membership (member invite & accepted) creation if (eventType === "organizationMembership.created") { try { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/CreateOrganizationMembership // Show what evnt?.data sends from above resource const { organization, public_user_data } = evnt?.data; console.log("created", evnt?.data); // @ts-ignore await addMemberToCommunity(organization.id, public_user_data.user_id); return NextResponse.json( { message: "Invitation accepted" }, { status: 201 } ); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } // Listen member deletion event if (eventType === "organizationMembership.deleted") { try { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/DeleteOrganizationMembership // Show what evnt?.data sends from above resource const { organization, public_user_data } = evnt?.data; console.log("removed", evnt?.data); // @ts-ignore await removeUserFromCommunity(public_user_data.user_id, organization.id); return NextResponse.json({ message: "Member removed" }, { status: 201 }); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } // Listen organization updation event if (eventType === "organization.updated") { try { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/UpdateOrganization // Show what evnt?.data sends from above resource const { id, logo_url, name, slug } = evnt?.data; console.log("updated", evnt?.data); // @ts-ignore await updateCommunityInfo(id, name, slug, logo_url); return NextResponse.json({ message: "Member removed" }, { status: 201 }); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } // Listen organization deletion event if (eventType === "organization.deleted") { try { // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/DeleteOrganization // Show what evnt?.data sends from above resource const { id } = evnt?.data; console.log("deleted", evnt?.data); // @ts-ignore await deleteCommunity(id); return NextResponse.json( { message: "Organization deleted" }, { status: 201 } ); } catch (err) { console.log(err); return NextResponse.json( { message: "Internal Server Error" }, { status: 500 } ); } } }; ``` </details> <details> <summary><code>community.actions.ts</code></summary> ```typescript "use server"; import { FilterQuery, SortOrder } from "mongoose"; import Community from "../models/community.model"; import Thread from "../models/thread.model"; import User from "../models/user.model"; import { connectToDB } from "../mongoose"; export async function createCommunity( id: string, name: string, username: string, image: string, bio: string, createdById: string // Change the parameter name to reflect it's an id ) { try { connectToDB(); // Find the user with the provided unique id const user = await User.findOne({ id: createdById }); if (!user) { throw new Error("User not found"); // Handle the case if the user with the id is not found } const newCommunity = new Community({ id, name, username, image, bio, createdBy: user._id, // Use the mongoose ID of the user }); const createdCommunity = await newCommunity.save(); // Update User model user.communities.push(createdCommunity._id); await user.save(); return createdCommunity; } catch (error) { // Handle any errors console.error("Error creating community:", error); throw error; } } export async function fetchCommunityDetails(id: string) { try { connectToDB(); const communityDetails = await Community.findOne({ id }).populate([ "createdBy", { path: "members", model: User, select: "name username image _id id", }, ]); return communityDetails; } catch (error) { // Handle any errors console.error("Error fetching community details:", error); throw error; } } export async function fetchCommunityPosts(id: string) { try { connectToDB(); const communityPosts = await Community.findById(id).populate({ path: "threads", model: Thread, populate: [ { path: "author", model: User, select: "name image id", // Select the "name" and "_id" fields from the "User" model }, { path: "children", model: Thread, populate: { path: "author", model: User, select: "image _id", // Select the "name" and "_id" fields from the "User" model }, }, ], }); return communityPosts; } catch (error) { // Handle any errors console.error("Error fetching community posts:", error); throw error; } } export async function fetchCommunities({ searchString = "", pageNumber = 1, pageSize = 20, sortBy = "desc", }: { searchString?: string; pageNumber?: number; pageSize?: number; sortBy?: SortOrder; }) { try { connectToDB(); // Calculate the number of communities to skip based on the page number and page size. const skipAmount = (pageNumber - 1) * pageSize; // Create a case-insensitive regular expression for the provided search string. const regex = new RegExp(searchString, "i"); // Create an initial query object to filter communities. const query: FilterQuery<typeof Community> = {}; // If the search string is not empty, add the $or operator to match either username or name fields. if (searchString.trim() !== "") { query.$or = [ { username: { $regex: regex } }, { name: { $regex: regex } }, ]; } // Define the sort options for the fetched communities based on createdAt field and provided sort order. const sortOptions = { createdAt: sortBy }; // Create a query to fetch the communities based on the search and sort criteria. const communitiesQuery = Community.find(query) .sort(sortOptions) .skip(skipAmount) .limit(pageSize) .populate("members"); // Count the total number of communities that match the search criteria (without pagination). const totalCommunitiesCount = await Community.countDocuments(query); const communities = await communitiesQuery.exec(); // Check if there are more communities beyond the current page. const isNext = totalCommunitiesCount > skipAmount + communities.length; return { communities, isNext }; } catch (error) { console.error("Error fetching communities:", error); throw error; } } export async function addMemberToCommunity( communityId: string, memberId: string ) { try { connectToDB(); // Find the community by its unique id const community = await Community.findOne({ id: communityId }); if (!community) { throw new Error("Community not found"); } // Find the user by their unique id const user = await User.findOne({ id: memberId }); if (!user) { throw new Error("User not found"); } // Check if the user is already a member of the community if (community.members.includes(user._id)) { throw new Error("User is already a member of the community"); } // Add the user's _id to the members array in the community community.members.push(user._id); await community.save(); // Add the community's _id to the communities array in the user user.communities.push(community._id); await user.save(); return community; } catch (error) { // Handle any errors console.error("Error adding member to community:", error); throw error; } } export async function removeUserFromCommunity( userId: string, communityId: string ) { try { connectToDB(); const userIdObject = await User.findOne({ id: userId }, { _id: 1 }); const communityIdObject = await Community.findOne( { id: communityId }, { _id: 1 } ); if (!userIdObject) { throw new Error("User not found"); } if (!communityIdObject) { throw new Error("Community not found"); } // Remove the user's _id from the members array in the community await Community.updateOne( { _id: communityIdObject._id }, { $pull: { members: userIdObject._id } } ); // Remove the community's _id from the communities array in the user await User.updateOne( { _id: userIdObject._id }, { $pull: { communities: communityIdObject._id } } ); return { success: true }; } catch (error) { // Handle any errors console.error("Error removing user from community:", error); throw error; } } export async function updateCommunityInfo( communityId: string, name: string, username: string, image: string ) { try { connectToDB(); // Find the community by its _id and update the information const updatedCommunity = await Community.findOneAndUpdate( { id: communityId }, { name, username, image } ); if (!updatedCommunity) { throw new Error("Community not found"); } return updatedCommunity; } catch (error) { // Handle any errors console.error("Error updating community information:", error); throw error; } } export async function deleteCommunity(communityId: string) { try { connectToDB(); // Find the community by its ID and delete it const deletedCommunity = await Community.findOneAndDelete({ id: communityId, }); if (!deletedCommunity) { throw new Error("Community not found"); } // Delete all threads associated with the community await Thread.deleteMany({ community: communityId }); // Find all users who are part of the community const communityUsers = await User.find({ communities: communityId }); // Remove the community from the 'communities' array for each user const updateUserPromises = communityUsers.map((user) => { user.communities.pull(communityId); return user.save(); }); await Promise.all(updateUserPromises); return deletedCommunity; } catch (error) { console.error("Error deleting community: ", error); throw error; } } ``` </details> <details> <summary><code>CommunityCard.tsx</code></summary> ```typescript import Image from "next/image"; import Link from "next/link"; import { Button } from "../ui/button"; interface Props { id: string; name: string; username: string; imgUrl: string; bio: string; members: { image: string; }[]; } function CommunityCard({ id, name, username, imgUrl, bio, members }: Props) { return ( <article className='community-card'> <div className='flex flex-wrap items-center gap-3'> <Link href={`/communities/${id}`} className='relative h-12 w-12'> <Image src={imgUrl} alt='community_logo' fill className='rounded-full object-cover' /> </Link> <div> <Link href={`/communities/${id}`}> <h4 className='text-base-semibold text-light-1'>{name}</h4> </Link> <p className='text-small-medium text-gray-1'>@{username}</p> </div> </div> <p className='mt-4 text-subtle-medium text-gray-1'>{bio}</p> <div className='mt-5 flex flex-wrap items-center justify-between gap-3'> <Link href={`/communities/${id}`}> <Button size='sm' className='community-card_btn'> View </Button> </Link> {members.length > 0 && ( <div className='flex items-center'> {members.map((member, index) => ( <Image key={index} src={member.image} alt={`user_${index}`} width={28} height={28} className={`${ index !== 0 && "-ml-2" } rounded-full object-cover`} /> ))} {members.length > 3 && ( <p className='ml-1 text-subtle-medium text-gray-1'> {members.length}+ Users </p> )} </div> )} </div> </article> ); } export default CommunityCard; ``` </details> <details> <summary><code>constants.index.ts</code></summary> ```typescript export const sidebarLinks = [ { imgURL: "/assets/home.svg", route: "/", label: "Home", }, { imgURL: "/assets/search.svg", route: "/search", label: "Search", }, { imgURL: "/assets/heart.svg", route: "/activity", label: "Activity", }, { imgURL: "/assets/create.svg", route: "/create-thread", label: "Create Thread", }, { imgURL: "/assets/community.svg", route: "/communities", label: "Communities", }, { imgURL: "/assets/user.svg", route: "/profile", label: "Profile", }, ]; export const profileTabs = [ { value: "threads", label: "Threads", icon: "/assets/reply.svg" }, { value: "replies", label: "Replies", icon: "/assets/members.svg" }, { value: "tagged", label: "Tagged", icon: "/assets/tag.svg" }, ]; export const communityTabs = [ { value: "threads", label: "Threads", icon: "/assets/reply.svg" }, { value: "members", label: "Members", icon: "/assets/members.svg" }, { value: "requests", label: "Requests", icon: "/assets/request.svg" }, ]; ``` </details> <details> <summary><code>globals.css</code></summary> ```css @tailwind base; @tailwind components; @tailwind utilities; @layer components { /* main */ .main-container { @apply flex min-h-screen flex-1 flex-col items-center bg-dark-1 px-6 pb-10 pt-28 max-md:pb-32 sm:px-10; } /* Head Text */ .head-text { @apply text-heading2-bold text-light-1; } /* Activity */ .activity-card { @apply flex items-center gap-2 rounded-md bg-dark-2 px-7 py-4; } /* No Result */ .no-result { @apply text-center !text-base-regular text-light-3; } /* Community Card */ .community-card { @apply w-full rounded-lg bg-dark-3 px-4 py-5 sm:w-96; } .community-card_btn { @apply rounded-lg bg-primary-500 px-5 py-1.5 text-small-regular !text-light-1 !important; } /* thread card */ .thread-card_bar { @apply relative mt-2 w-0.5 grow rounded-full bg-neutral-800; } /* User card */ .user-card { @apply flex flex-col justify-between gap-4 max-xs:rounded-xl max-xs:bg-dark-3 max-xs:p-4 xs:flex-row xs:items-center; } .user-card_avatar { @apply flex flex-1 items-start justify-start gap-3 xs:items-center; } .user-card_btn { @apply h-auto min-w-[74px] rounded-lg bg-primary-500 text-[12px] text-light-1 !important; } .searchbar { @apply flex gap-1 rounded-lg bg-dark-3 px-4 py-2; } .searchbar_input { @apply border-none bg-dark-3 text-base-regular text-light-4 outline-none !important; } .topbar { @apply fixed top-0 z-30 flex w-full items-center justify-between bg-dark-2 px-6 py-3; } .bottombar { @apply fixed bottom-0 z-10 w-full rounded-t-3xl bg-glassmorphism p-4 backdrop-blur-lg xs:px-7 md:hidden; } .bottombar_container { @apply flex items-center justify-between gap-3 xs:gap-5; } .bottombar_link { @apply relative flex flex-col items-center gap-2 rounded-lg p-2 sm:flex-1 sm:px-2 sm:py-2.5; } .leftsidebar { @apply sticky left-0 top-0 z-20 flex h-screen w-fit flex-col justify-between overflow-auto border-r border-r-dark-4 bg-dark-2 pb-5 pt-28 max-md:hidden; } .leftsidebar_link { @apply relative flex justify-start gap-4 rounded-lg p-4; } .pagination { @apply mt-10 flex w-full items-center justify-center gap-5; } .rightsidebar { @apply sticky right-0 top-0 z-20 flex h-screen w-fit flex-col justify-between gap-12 overflow-auto border-l border-l-dark-4 bg-dark-2 px-10 pb-6 pt-28 max-xl:hidden; } } @layer utilities { .css-invert { @apply invert-[50%] brightness-200; } .custom-scrollbar::-webkit-scrollbar { width: 3px; height: 3px; border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-track { background: #09090a; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #5c5c7b; border-radius: 50px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #7878a3; } } /* Clerk Responsive fix */ .cl-organizationSwitcherTrigger .cl-userPreview .cl-userPreviewTextContainer { @apply max-sm:hidden; } .cl-organizationSwitcherTrigger .cl-organizationPreview .cl-organizationPreviewTextContainer { @apply max-sm:hidden; } /* Shadcn Component Styles */ /* Tab */ .tab { @apply flex min-h-[50px] flex-1 items-center gap-3 bg-dark-2 text-light-2 data-[state=active]:bg-[#0e0e12] data-[state=active]:text-light-2 !important; } .no-focus { @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important; } /* Account Profile */ .account-form_image-label { @apply flex h-24 w-24 items-center justify-center rounded-full bg-dark-4 !important; } .account-form_image-input { @apply cursor-pointer border-none bg-transparent outline-none file:text-blue !important; } .account-form_input { @apply border border-dark-4 bg-dark-3 text-light-1 !important; } /* Comment Form */ .comment-form { @apply mt-10 flex items-center gap-4 border-y border-y-dark-4 py-5 max-xs:flex-col !important; } .comment-form_btn { @apply rounded-3xl bg-primary-500 px-8 py-2 !text-small-regular text-light-1 max-xs:w-full !important; } ``` </details> <details> <summary><code>next.config.js</code></summary> ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { serverActions: true, serverComponentsExternalPackages: ["mongoose"], }, images: { remotePatterns: [ { protocol: "https", hostname: "img.clerk.com", }, { protocol: "https", hostname: "images.clerk.dev", }, { protocol: "https", hostname: "uploadthing.com", }, { protocol: "https", hostname: "placehold.co", }, ], typescript: { ignoreBuildErrors: true, }, }, }; module.exports = nextConfig; ``` </details> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, fontSize: { "heading1-bold": [ "36px", { lineHeight: "140%", fontWeight: "700", }, ], "heading1-semibold": [ "36px", { lineHeight: "140%", fontWeight: "600", }, ], "heading2-bold": [ "30px", { lineHeight: "140%", fontWeight: "700", }, ], "heading2-semibold": [ "30px", { lineHeight: "140%", fontWeight: "600", }, ], "heading3-bold": [ "24px", { lineHeight: "140%", fontWeight: "700", }, ], "heading4-medium": [ "20px", { lineHeight: "140%", fontWeight: "500", }, ], "body-bold": [ "18px", { lineHeight: "140%", fontWeight: "700", }, ], "body-semibold": [ "18px", { lineHeight: "140%", fontWeight: "600", }, ], "body-medium": [ "18px", { lineHeight: "140%", fontWeight: "500", }, ], "body-normal": [ "18px", { lineHeight: "140%", fontWeight: "400", }, ], "body1-bold": [ "18px", { lineHeight: "140%", fontWeight: "700", }, ], "base-regular": [ "16px", { lineHeight: "140%", fontWeight: "400", }, ], "base-medium": [ "16px", { lineHeight: "140%", fontWeight: "500", }, ], "base-semibold": [ "16px", { lineHeight: "140%", fontWeight: "600", }, ], "base1-semibold": [ "16px", { lineHeight: "140%", fontWeight: "600", }, ], "small-regular": [ "14px", { lineHeight: "140%", fontWeight: "400", }, ], "small-medium": [ "14px", { lineHeight: "140%", fontWeight: "500", }, ], "small-semibold": [ "14px", { lineHeight: "140%", fontWeight: "600", }, ], "subtle-medium": [ "12px", { lineHeight: "16px", fontWeight: "500", }, ], "subtle-semibold": [ "12px", { lineHeight: "16px", fontWeight: "600", }, ], "tiny-medium": [ "10px", { lineHeight: "140%", fontWeight: "500", }, ], "x-small-semibold": [ "7px", { lineHeight: "9.318px", fontWeight: "600", }, ], }, extend: { colors: { "primary-500": "#877EFF", "secondary-500": "#FFB620", blue: "#0095F6", "logout-btn": "#FF5A5A", "navbar-menu": "rgba(16, 16, 18, 0.6)", "dark-1": "#000000", "dark-2": "#121417", "dark-3": "#101012", "dark-4": "#1F1F22", "light-1": "#FFFFFF", "light-2": "#EFEFEF", "light-3": "#7878A3", "light-4": "#5C5C7B", "gray-1": "#697C89", glassmorphism: "rgba(16, 16, 18, 0.60)", }, boxShadow: { "count-badge": "0px 0px 6px 2px rgba(219, 188, 159, 0.30)", "groups-sidebar": "-30px 0px 60px 0px rgba(28, 28, 31, 0.50)", }, screens: { xs: "400px", }, keyframes: { "accordion-down": { from: { height: 0 }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: 0 }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], }; ``` </details> <details> <summary><code>thread.actions.ts</code></summary> ```typescript "use server"; import { revalidatePath } from "next/cache"; import { connectToDB } from "../mongoose"; import User from "../models/user.model"; import Thread from "../models/thread.model"; import Community from "../models/community.model"; export async function fetchPosts(pageNumber = 1, pageSize = 20) { connectToDB(); // Calculate the number of posts to skip based on the page number and page size. const skipAmount = (pageNumber - 1) * pageSize; // Create a query to fetch the posts that have no parent (top-level threads) (a thread that is not a comment/reply). const postsQuery = Thread.find({ parentId: { $in: [null, undefined] } }) .sort({ createdAt: "desc" }) .skip(skipAmount) .limit(pageSize) .populate({ path: "author", model: User, }) .populate({ path: "community", model: Community, }) .populate({ path: "children", // Populate the children field populate: { path: "author", // Populate the author field within children model: User, select: "_id name parentId image", // Select only _id and username fields of the author }, }); // Count the total number of top-level posts (threads) i.e., threads that are not comments. const totalPostsCount = await Thread.countDocuments({ parentId: { $in: [null, undefined] }, }); // Get the total count of posts const posts = await postsQuery.exec(); const isNext = totalPostsCount > skipAmount + posts.length; return { posts, isNext }; } interface Params { text: string, author: string, communityId: string | null, path: string, } export async function createThread({ text, author, communityId, path }: Params ) { try { connectToDB(); const communityIdObject = await Community.findOne( { id: communityId }, { _id: 1 } ); const createdThread = await Thread.create({ text, author, community: communityIdObject, // Assign communityId if provided, or leave it null for personal account }); // Update User model await User.findByIdAndUpdate(author, { $push: { threads: createdThread._id }, }); if (communityIdObject) { // Update Community model await Community.findByIdAndUpdate(communityIdObject, { $push: { threads: createdThread._id }, }); } revalidatePath(path); } catch (error: any) { throw new Error(`Failed to create thread: ${error.message}`); } } async function fetchAllChildThreads(threadId: string): Promise<any[]> { const childThreads = await Thread.find({ parentId: threadId }); const descendantThreads = []; for (const childThread of childThreads) { const descendants = await fetchAllChildThreads(childThread._id); descendantThreads.push(childThread, ...descendants); } return descendantThreads; } export async function deleteThread(id: string, path: string): Promise<void> { try { connectToDB(); // Find the thread to be deleted (the main thread) const mainThread = await Thread.findById(id).populate("author community"); if (!mainThread) { throw new Error("Thread not found"); } // Fetch all child threads and their descendants recursively const descendantThreads = await fetchAllChildThreads(id); // Get all descendant thread IDs including the main thread ID and child thread IDs const descendantThreadIds = [ id, ...descendantThreads.map((thread) => thread._id), ]; // Extract the authorIds and communityIds to update User and Community models respectively const uniqueAuthorIds = new Set( [ ...descendantThreads.map((thread) => thread.author?._id?.toString()), // Use optional chaining to handle possible undefined values mainThread.author?._id?.toString(), ].filter((id) => id !== undefined) ); const uniqueCommunityIds = new Set( [ ...descendantThreads.map((thread) => thread.community?._id?.toString()), // Use optional chaining to handle possible undefined values mainThread.community?._id?.toString(), ].filter((id) => id !== undefined) ); // Recursively delete child threads and their descendants await Thread.deleteMany({ _id: { $in: descendantThreadIds } }); // Update User model await User.updateMany( { _id: { $in: Array.from(uniqueAuthorIds) } }, { $pull: { threads: { $in: descendantThreadIds } } } ); // Update Community model await Community.updateMany( { _id: { $in: Array.from(uniqueCommunityIds) } }, { $pull: { threads: { $in: descendantThreadIds } } } ); revalidatePath(path); } catch (error: any) { throw new Error(`Failed to delete thread: ${error.message}`); } } export async function fetchThreadById(threadId: string) { connectToDB(); try { const thread = await Thread.findById(threadId) .populate({ path: "author", model: User, select: "_id id name image", }) // Populate the author field with _id and username .populate({ path: "community", model: Community, select: "_id id name image", }) // Populate the community field with _id and name .populate({ path: "children", // Populate the children field populate: [ { path: "author", // Populate the author field within children model: User, select: "_id id name parentId image", // Select only _id and username fields of the author }, { path: "children", // Populate the children field within children model: Thread, // The model of the nested children (assuming it's the same "Thread" model) populate: { path: "author", // Populate the author field within nested children model: User, select: "_id id name parentId image", // Select only _id and username fields of the author }, }, ], }) .exec(); return thread; } catch (err) { console.error("Error while fetching thread:", err); throw new Error("Unable to fetch thread"); } } export async function addCommentToThread( threadId: string, commentText: string, userId: string, path: string ) { connectToDB(); try { // Find the original thread by its ID const originalThread = await Thread.findById(threadId); if (!originalThread) { throw new Error("Thread not found"); } // Create the new comment thread const commentThread = new Thread({ text: commentText, author: userId, parentId: threadId, // Set the parentId to the original thread's ID }); // Save the comment thread to the database const savedCommentThread = await commentThread.save(); // Add the comment thread's ID to the original thread's children array originalThread.children.push(savedCommentThread._id); // Save the updated original thread to the database await originalThread.save(); revalidatePath(path); } catch (err) { console.error("Error while adding comment:", err); throw new Error("Unable to add comment"); } } ``` </details> <details> <summary><code>uploadthing.ts</code></summary> ```typescript // Resource: https://docs.uploadthing.com/api-reference/react#generatereacthelpers // Copy paste (be careful with imports) import { generateReactHelpers } from "@uploadthing/react/hooks"; import type { OurFileRouter } from "@/app/api/uploadthing/core"; export const { useUploadThing, uploadFiles } = generateReactHelpers<OurFileRouter>(); ``` </details> <details> <summary><code>user.actions.ts</code></summary> ```typescript "use server"; import { FilterQuery, SortOrder } from "mongoose"; import { revalidatePath } from "next/cache"; import Community from "../models/community.model"; import Thread from "../models/thread.model"; import User from "../models/user.model"; import { connectToDB } from "../mongoose"; export async function fetchUser(userId: string) { try { connectToDB(); return await User.findOne({ id: userId }).populate({ path: "communities", model: Community, }); } catch (error: any) { throw new Error(`Failed to fetch user: ${error.message}`); } } interface Params { userId: string; username: string; name: string; bio: string; image: string; path: string; } export async function updateUser({ userId, bio, name, path, username, image, }: Params): Promise<void> { try { connectToDB(); await User.findOneAndUpdate( { id: userId }, { username: username.toLowerCase(), name, bio, image, onboarded: true, }, { upsert: true } ); if (path === "/profile/edit") { revalidatePath(path); } } catch (error: any) { throw new Error(`Failed to create/update user: ${error.message}`); } } export async function fetchUserPosts(userId: string) { try { connectToDB(); // Find all threads authored by the user with the given userId const threads = await User.findOne({ id: userId }).populate({ path: "threads", model: Thread, populate: [ { path: "community", model: Community, select: "name id image _id", // Select the "name" and "_id" fields from the "Community" model }, { path: "children", model: Thread, populate: { path: "author", model: User, select: "name image id", // Select the "name" and "_id" fields from the "User" model }, }, ], }); return threads; } catch (error) { console.error("Error fetching user threads:", error); throw error; } } // Almost similar to Thead (search + pagination) and Community (search + pagination) export async function fetchUsers({ userId, searchString = "", pageNumber = 1, pageSize = 20, sortBy = "desc", }: { userId: string; searchString?: string; pageNumber?: number; pageSize?: number; sortBy?: SortOrder; }) { try { connectToDB(); // Calculate the number of users to skip based on the page number and page size. const skipAmount = (pageNumber - 1) * pageSize; // Create a case-insensitive regular expression for the provided search string. const regex = new RegExp(searchString, "i"); // Create an initial query object to filter users. const query: FilterQuery<typeof User> = { id: { $ne: userId }, // Exclude the current user from the results. }; // If the search string is not empty, add the $or operator to match either username or name fields. if (searchString.trim() !== "") { query.$or = [ { username: { $regex: regex } }, { name: { $regex: regex } }, ]; } // Define the sort options for the fetched users based on createdAt field and provided sort order. const sortOptions = { createdAt: sortBy }; const usersQuery = User.find(query) .sort(sortOptions) .skip(skipAmount) .limit(pageSize); // Count the total number of users that match the search criteria (without pagination). const totalUsersCount = await User.countDocuments(query); const users = await usersQuery.exec(); // Check if there are more users beyond the current page. const isNext = totalUsersCount > skipAmount + users.length; return { users, isNext }; } catch (error) { console.error("Error fetching users:", error); throw error; } } export async function getActivity(userId: string) { try { connectToDB(); // Find all threads created by the user const userThreads = await Thread.find({ author: userId }); // Collect all the child thread ids (replies) from the 'children' field of each user thread const childThreadIds = userThreads.reduce((acc, userThread) => { return acc.concat(userThread.children); }, []); // Find and return the child threads (replies) excluding the ones created by the same user const replies = await Thread.find({ _id: { $in: childThreadIds }, author: { $ne: userId }, // Exclude threads authored by the same user }).populate({ path: "author", model: User, select: "name image _id", }); return replies; } catch (error) { console.error("Error fetching replies: ", error); throw error; } } ``` </details> <details> <summary><code>utils.ts</code></summary> ```typescript import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; // generated by shadcn export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // created by chatgpt export function isBase64Image(imageData: string) { const base64Regex = /^data:image\/(png|jpe?g|gif|webp);base64,/; return base64Regex.test(imageData); } // created by chatgpt export function formatDateString(dateString: string) { const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", }; const date = new Date(dateString); const formattedDate = date.toLocaleDateString(undefined, options); const time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); return `${time} - ${formattedDate}`; } // created by chatgpt export function formatThreadCount(count: number): string { if (count === 0) { return "No Threads"; } else { const threadCount = count.toString().padStart(2, "0"); const threadWord = count === 1 ? "Thread" : "Threads"; return `${threadCount} ${threadWord}`; } } ``` </details> ## <a name="links">πŸ”— Links</a> Assets used in the project are [here](https://drive.google.com/file/d/1lg7MMKgXwFabymHi1qxRYMxWVXiZPM9l/view) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Web Development Community & Forums
1.6K Github Stars
ai_saas_app
Open Source

ai_saas_app

<div align="center"> <br /> <a href="https://youtu.be/Ahwoks_dawU?feature=shared" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/daf9e91b-6342-4e9a-9361-8dc2bd01ce64" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Stripe-black?style=for-the-badge&logoColor=white&logo=stripe&color=008CDD" alt="stripe" /> <img src="https://img.shields.io/badge/-MongoDB-black?style=for-the-badge&logoColor=white&logo=mongodb&color=47A248" alt="mongodb" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">An AI SaaS Platform</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/Ahwoks_dawU?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Build an AI image SaaS platform that excels in image processing capabilities, integrates a secure payment infrastructure, offers advanced image search functionalities, and supports multiple AI features, including image restoration, recoloring, object removal, generative filling, and background removal. This project can be a guide for your next AI image tool and a boost to your portfolio. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - TypeScript - MongoDB - Clerk - Cloudinary - Stripe - Shadcn - TailwindCSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Authentication and Authorization**: Secure user access with registration, login, and route protection. πŸ‘‰ **Community Image Showcase**: Explore user transformations with easy navigation using pagination πŸ‘‰ **Advanced Image Search**: Find images by content or objects present inside the image quickly and accurately πŸ‘‰ **Image Restoration**: Revive old or damaged images effortlessly πŸ‘‰ **Image Recoloring**: Customize images by replacing objects with desired colors easily πŸ‘‰ **Image Generative Fill**: Fill in missing areas of images seamlessly πŸ‘‰ **Object Removal**: Clean up images by removing unwanted objects with precision πŸ‘‰ **Background Removal**: Extract objects from backgrounds with ease πŸ‘‰ **Download Transformed Images**: Save and share AI-transformed images conveniently πŸ‘‰ **Transformed Image Details**: View details of transformations for each image πŸ‘‰ **Transformation Management**: Control over deletion and updates of transformations πŸ‘‰ **Credits System**: Earn or purchase credits for image transformations πŸ‘‰ **Profile Page**: Access transformed images and credit information personally πŸ‘‰ **Credits Purchase**: Securely buy credits via Stripe for uninterrupted use πŸ‘‰ **Responsive UI/UX**: A seamless experience across devices with a user-friendly interface and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/imaginify.git cd imaginify ``` **Installation** Install the project dependencies using npm: ```bash npm run dev ``` **Set Up Environment Variables** Create a new file named `.env.local` in the root of your project and add the following content: ```env #NEXT NEXT_PUBLIC_SERVER_URL= #MONGODB MONGODB_URL= #CLERK NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= WEBHOOK_SECRET= NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ #CLOUDINARY NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= CLOUDINARY_API_KEY= CLOUDINARY_API_SECRET= #STRIPE STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= ``` Replace the placeholder values with your actual respective account credentials. You can obtain these credentials by signing up on the [Clerk](https://clerk.com/), [MongoDB](https://www.mongodb.com/), [Cloudinary](https://cloudinary.com/) and [Stripe](https://stripe.com) **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", purple: { 100: "#F4F7FE", 200: "#BCB6FF", 400: "#868CFF", 500: "#7857FF", 600: "#4318FF", }, dark: { 400: "#7986AC", 500: "#606C80", 600: "#2B3674", 700: "#384262", }, primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, fontFamily: { IBMPlex: ["var(--font-ibm-plex)"], }, backgroundImage: { "purple-gradient": "url('/assets/images/gradient-bg.svg')", banner: "url('/assets/images/banner-bg.png')", }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], }; ``` </details> <details> <summary><code>globals.css</code></summary> ```css @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } .auth { @apply flex-center min-h-screen w-full bg-purple-100 } .root { @apply flex min-h-screen w-full flex-col bg-white lg:flex-row; } .root-container { @apply mt-16 flex-1 overflow-auto py-8 lg:mt-0 lg:max-h-screen lg:py-10 } /* ========================================== TAILWIND STYLES */ @layer utilities { /* ===== UTILITIES */ .wrapper { @apply max-w-5xl mx-auto px-5 md:px-10 w-full text-dark-400 p-16-regular; } .gradient-text { @apply bg-purple-gradient bg-cover bg-clip-text text-transparent; } /* ===== ALIGNMENTS */ .flex-center { @apply flex justify-center items-center; } .flex-between { @apply flex justify-between items-center; } /* ===== TYPOGRAPHY */ /* 44 */ .h1-semibold { @apply text-[36px] font-semibold sm:text-[44px] leading-[120%] sm:leading-[56px]; } /* 36 */ .h2-bold { @apply text-[30px] font-bold md:text-[36px] leading-[110%]; } /* 30 */ .h3-bold { @apply font-bold text-[30px] leading-[140%]; } /* 24 */ .p-24-bold { @apply font-bold text-[24px] leading-[120%]; } /* 20 */ .p-20-semibold { @apply font-semibold text-[20px] leading-[140%]; } .p-20-regular { @apply font-normal text-[20px] leading-[140%]; } /* 18 */ .p-18-semibold { @apply font-semibold text-[18px] leading-[140%]; } /* 16 */ .p-16-semibold { @apply font-semibold text-[16px] leading-[140%]; } .p-16-medium { @apply font-medium text-[16px] leading-[140%]; } .p-16-regular { @apply font-normal text-[16px] leading-[140%]; } /* 14 */ .p-14-medium { @apply font-medium text-[14px] leading-[120%]; } /* 10 */ .p-10-medium { @apply font-medium text-[10px] leading-[140%]; } /* ===== SHADCN OVERRIDES */ .button { @apply py-4 px-6 flex-center gap-3 rounded-full p-16-semibold focus-visible:ring-offset-0 focus-visible:ring-transparent !important; } .dropdown-content { @apply shadow-lg rounded-md overflow-hidden p-0; } .dropdown-item { @apply p-16-semibold text-dark-700 cursor-pointer transition-all px-4 py-3 rounded-none outline-none hover:border-none focus-visible:ring-transparent hover:text-white hover:bg-purple-gradient hover:bg-cover focus-visible:ring-offset-0 focus-visible:outline-none !important; } .input-field { @apply rounded-[16px] border-2 border-purple-200/20 shadow-sm shadow-purple-200/15 text-dark-600 disabled:opacity-100 p-16-semibold h-[50px] md:h-[54px] focus-visible:ring-offset-0 px-4 py-3 focus-visible:ring-transparent !important; } .search-field { @apply border-0 bg-transparent text-dark-600 w-full placeholder:text-dark-400 h-[50px] p-16-medium focus-visible:ring-offset-0 p-3 focus-visible:ring-transparent !important; } .submit-button { @apply bg-purple-gradient bg-cover rounded-full py-4 px-6 p-16-semibold h-[50px] w-full md:h-[54px]; } .select-field { @apply w-full border-2 border-purple-200/20 shadow-sm shadow-purple-200/15 rounded-[16px] h-[50px] md:h-[54px] text-dark-600 p-16-semibold disabled:opacity-100 placeholder:text-dark-400/50 px-4 py-3 focus:ring-offset-0 focus-visible:ring-transparent focus:ring-transparent focus-visible:ring-0 focus-visible:outline-none !important; } .select-trigger { @apply flex items-center gap-2 py-5 capitalize focus-visible:outline-none; } .select-item { @apply py-3 cursor-pointer hover:bg-purple-100; } .IconButton { @apply focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important; } .sheet-content button { @apply focus:ring-0 focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important; } .success-toast { @apply bg-green-100 text-green-900; } .error-toast { @apply bg-red-100 text-red-900; } /* Home Page */ .home { @apply sm:flex-center hidden h-72 flex-col gap-4 rounded-[20px] border bg-banner bg-cover bg-no-repeat p-10 shadow-inner; } .home-heading { @apply h1-semibold max-w-[500px] flex-wrap text-center text-white shadow-sm; } /* Credits Page */ .credits-list { @apply mt-11 grid grid-cols-1 gap-5 sm:grid-cols-2 md:gap-9 xl:grid-cols-3; } .credits-item { @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-8 shadow-xl shadow-purple-200/20 lg:max-w-none; } .credits-btn { @apply w-full rounded-full bg-purple-100 bg-cover text-purple-500 hover:text-purple-500; } /* Profile Page */ .profile { @apply mt-5 flex flex-col gap-5 sm:flex-row md:mt-8 md:gap-10; } .profile-balance { @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-5 shadow-lg shadow-purple-200/10 md:px-6 md:py-8; } .profile-image-manipulation { @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-5 shadow-lg shadow-purple-200/10 md:px-6 md:py-8; } /* Transformation Details */ .transformation-grid { @apply grid h-fit min-h-[200px] grid-cols-1 gap-5 py-8 md:grid-cols-2; } .transformation-original_image { @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2; } /* Collection Component */ .collection-heading { @apply md:flex-between mb-6 flex flex-col gap-5 md:flex-row; } .collection-list { @apply grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3; } .collection-empty { @apply flex-center h-60 w-full rounded-[10px] border border-dark-400/10 bg-white/20; } .collection-btn { @apply button w-32 bg-purple-gradient bg-cover text-white; } .collection-card { @apply flex flex-1 cursor-pointer flex-col gap-5 rounded-[16px] border-2 border-purple-200/15 bg-white p-4 shadow-xl shadow-purple-200/10 transition-all hover:shadow-purple-200/20; } /* MediaUploader Component */ .media-uploader_cldImage { @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2; } .media-uploader_cta { @apply flex-center flex h-72 cursor-pointer flex-col gap-5 rounded-[16px] border border-dashed bg-purple-100/20 shadow-inner; } .media-uploader_cta-image { @apply rounded-[16px] bg-white p-5 shadow-sm shadow-purple-200/50; } /* Navbar Component */ .header { @apply flex-between fixed h-16 w-full border-b-4 border-purple-100 bg-white p-5 lg:hidden; } .header-nav_elements { @apply mt-8 flex w-full flex-col items-start gap-5; } /* Search Component */ .search { @apply flex w-full rounded-[16px] border-2 border-purple-200/20 bg-white px-4 shadow-sm shadow-purple-200/15 md:max-w-96; } /* Sidebar Component */ .sidebar { @apply hidden h-screen w-72 bg-white p-5 shadow-md shadow-purple-200/50 lg:flex; } .sidebar-logo { @apply flex items-center gap-2 md:py-2; } .sidebar-nav { @apply h-full flex-col justify-between md:flex md:gap-4; } .sidebar-nav_elements { @apply hidden w-full flex-col items-start gap-2 md:flex; } .sidebar-nav_element { @apply flex-center p-16-semibold w-full whitespace-nowrap rounded-full bg-cover transition-all hover:bg-purple-100 hover:shadow-inner; } .sidebar-link { @apply p-16-semibold flex size-full gap-4 p-4; } /* TransformationForm Component */ .prompt-field { @apply flex flex-col gap-5 lg:flex-row lg:gap-10; } .media-uploader-field { @apply grid h-fit min-h-[200px] grid-cols-1 gap-5 py-4 md:grid-cols-2; } /* TransformedImage Component */ .download-btn { @apply p-14-medium mt-2 flex items-center gap-2 px-2; } .transformed-image { @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2; } .transforming-loader { @apply flex-center absolute left-[50%] top-[50%] size-full -translate-x-1/2 -translate-y-1/2 flex-col gap-2 rounded-[10px] border bg-dark-700/90; } .transformed-placeholder { @apply flex-center p-14-medium h-full min-h-72 flex-col gap-5 rounded-[16px] border border-dashed bg-purple-100/20 shadow-inner; } } /* ===== CLERK OVERRIDES */ .cl-userButtonBox { display: flex; flex-flow: row-reverse; gap: 12px; } .cl-userButtonOuterIdentifier { font-size: 16px; font-weight: 600; color: #384262; } ``` </details> <details> <summary><code>constants/index.ts</code></summary> ```typescript export const navLinks = [ { label: "Home", route: "/", icon: "/assets/icons/home.svg", }, { label: "Image Restore", route: "/transformations/add/restore", icon: "/assets/icons/image.svg", }, { label: "Generative Fill", route: "/transformations/add/fill", icon: "/assets/icons/stars.svg", }, { label: "Object Remove", route: "/transformations/add/remove", icon: "/assets/icons/scan.svg", }, { label: "Object Recolor", route: "/transformations/add/recolor", icon: "/assets/icons/filter.svg", }, { label: "Background Remove", route: "/transformations/add/removeBackground", icon: "/assets/icons/camera.svg", }, { label: "Profile", route: "/profile", icon: "/assets/icons/profile.svg", }, { label: "Buy Credits", route: "/credits", icon: "/assets/icons/bag.svg", }, ]; export const plans = [ { _id: 1, name: "Free", icon: "/assets/icons/free-plan.svg", price: 0, credits: 20, inclusions: [ { label: "20 Free Credits", isIncluded: true, }, { label: "Basic Access to Services", isIncluded: true, }, { label: "Priority Customer Support", isIncluded: false, }, { label: "Priority Updates", isIncluded: false, }, ], }, { _id: 2, name: "Pro Package", icon: "/assets/icons/free-plan.svg", price: 40, credits: 120, inclusions: [ { label: "120 Credits", isIncluded: true, }, { label: "Full Access to Services", isIncluded: true, }, { label: "Priority Customer Support", isIncluded: true, }, { label: "Priority Updates", isIncluded: false, }, ], }, { _id: 3, name: "Premium Package", icon: "/assets/icons/free-plan.svg", price: 199, credits: 2000, inclusions: [ { label: "2000 Credits", isIncluded: true, }, { label: "Full Access to Services", isIncluded: true, }, { label: "Priority Customer Support", isIncluded: true, }, { label: "Priority Updates", isIncluded: true, }, ], }, ]; export const transformationTypes = { restore: { type: "restore", title: "Restore Image", subTitle: "Refine images by removing noise and imperfections", config: { restore: true }, icon: "image.svg", }, removeBackground: { type: "removeBackground", title: "Background Remove", subTitle: "Removes the background of the image using AI", config: { removeBackground: true }, icon: "camera.svg", }, fill: { type: "fill", title: "Generative Fill", subTitle: "Enhance an image's dimensions using AI outpainting", config: { fillBackground: true }, icon: "stars.svg", }, remove: { type: "remove", title: "Object Remove", subTitle: "Identify and eliminate objects from images", config: { remove: { prompt: "", removeShadow: true, multiple: true }, }, icon: "scan.svg", }, recolor: { type: "recolor", title: "Object Recolor", subTitle: "Identify and recolor objects from the image", config: { recolor: { prompt: "", to: "", multiple: true }, }, icon: "filter.svg", }, }; export const aspectRatioOptions = { "1:1": { aspectRatio: "1:1", label: "Square (1:1)", width: 1000, height: 1000, }, "3:4": { aspectRatio: "3:4", label: "Standard Portrait (3:4)", width: 1000, height: 1334, }, "9:16": { aspectRatio: "9:16", label: "Phone Portrait (9:16)", width: 1000, height: 1778, }, }; export const defaultValues = { title: "", aspectRatio: "", color: "", prompt: "", publicId: "", }; export const creditFee = -1; ``` </details> <details> <summary><code>user.model.ts</code></summary> ```typescript import { Schema, model, models } from "mongoose"; const UserSchema = new Schema({ clerkId: { type: String, required: true, unique: true, }, email: { type: String, required: true, unique: true, }, username: { type: String, required: true, unique: true, }, photo: { type: String, required: true, }, firstName: { type: String, }, lastName: { type: String, }, planId: { type: Number, default: 1, }, creditBalance: { type: Number, default: 10, }, }); const User = models?.User || model("User", UserSchema); export default User; ``` </details> <details> <summary><code>transaction.model.ts</code></summary> ```typescript import { Schema, model, models } from "mongoose"; const TransactionSchema = new Schema({ createdAt: { type: Date, default: Date.now, }, stripeId: { type: String, required: true, unique: true, }, amount: { type: Number, required: true, }, plan: { type: String, }, credits: { type: Number, }, buyer: { type: Schema.Types.ObjectId, ref: "User", }, }); const Transaction = models?.Transaction || model("Transaction", TransactionSchema); export default Transaction; ``` </details> <details> <summary><code>InsufficientCreditsModal.tsx</code></summary> ```typescript "use client"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; export const InsufficientCreditsModal = () => { const router = useRouter(); return ( <AlertDialog defaultOpen> <AlertDialogContent> <AlertDialogHeader> <div className="flex-between"> <p className="p-16-semibold text-dark-400">Insufficient Credits</p> <AlertDialogCancel className="border-0 p-0 hover:bg-transparent" onClick={() => router.push("/profile")} > <Image src="/assets/icons/close.svg" alt="credit coins" width={24} height={24} className="cursor-pointer" /> </AlertDialogCancel> </div> <Image src="/assets/images/stacked-coins.png" alt="credit coins" width={462} height={122} /> <AlertDialogTitle className="p-24-bold text-dark-600"> Oops.... Looks like you&#39;ve run out of free credits! </AlertDialogTitle> <AlertDialogDescription className="p-16-regular py-3"> No worries, though - you can keep enjoying our services by grabbing more credits. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel className="button w-full bg-purple-100 text-dark-400" onClick={() => router.push("/profile")} > No, Cancel </AlertDialogCancel> <AlertDialogAction className="button w-full bg-purple-gradient bg-cover" onClick={() => router.push("/credits")} > Yes, Proceed </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); }; ``` </details> <details> <summary><code>user.action.ts</code></summary> ```typescript "use server"; import { revalidatePath } from "next/cache"; import User from "../database/models/user.model"; import { connectToDatabase } from "../database/mongoose"; import { handleError } from "../utils"; // CREATE export async function createUser(user: CreateUserParams) { try { await connectToDatabase(); const newUser = await User.create(user); return JSON.parse(JSON.stringify(newUser)); } catch (error) { handleError(error); } } // READ export async function getUserById(userId: string) { try { await connectToDatabase(); const user = await User.findOne({ clerkId: userId }); if (!user) throw new Error("User not found"); return JSON.parse(JSON.stringify(user)); } catch (error) { handleError(error); } } // UPDATE export async function updateUser(clerkId: string, user: UpdateUserParams) { try { await connectToDatabase(); const updatedUser = await User.findOneAndUpdate({ clerkId }, user, { new: true, }); if (!updatedUser) throw new Error("User update failed"); return JSON.parse(JSON.stringify(updatedUser)); } catch (error) { handleError(error); } } // DELETE export async function deleteUser(clerkId: string) { try { await connectToDatabase(); // Find user to delete const userToDelete = await User.findOne({ clerkId }); if (!userToDelete) { throw new Error("User not found"); } // Delete user const deletedUser = await User.findByIdAndDelete(userToDelete._id); revalidatePath("/"); return deletedUser ? JSON.parse(JSON.stringify(deletedUser)) : null; } catch (error) { handleError(error); } } // USE CREDITS export async function updateCredits(userId: string, creditFee: number) { try { await connectToDatabase(); const updatedUserCredits = await User.findOneAndUpdate( { _id: userId }, { $inc: { creditBalance: creditFee }}, { new: true } ) if(!updatedUserCredits) throw new Error("User credits update failed"); return JSON.parse(JSON.stringify(updatedUserCredits)); } catch (error) { handleError(error); } } ``` </details> <details> <summary><code>utils.ts</code></summary> ```typescript /* eslint-disable prefer-const */ /* eslint-disable no-prototype-builtins */ import { type ClassValue, clsx } from "clsx"; import qs from "qs"; import { twMerge } from "tailwind-merge"; import { aspectRatioOptions } from "@/constants"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // ERROR HANDLER export const handleError = (error: unknown) => { if (error instanceof Error) { // This is a native JavaScript error (e.g., TypeError, RangeError) console.error(error.message); throw new Error(`Error: ${error.message}`); } else if (typeof error === "string") { // This is a string error message console.error(error); throw new Error(`Error: ${error}`); } else { // This is an unknown type of error console.error(error); throw new Error(`Unknown error: ${JSON.stringify(error)}`); } }; // PLACEHOLDER LOADER - while image is transforming const shimmer = (w: number, h: number) => ` <svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <linearGradient id="g"> <stop stop-color="#7986AC" offset="20%" /> <stop stop-color="#68769e" offset="50%" /> <stop stop-color="#7986AC" offset="70%" /> </linearGradient> </defs> <rect width="${w}" height="${h}" fill="#7986AC" /> <rect id="r" width="${w}" height="${h}" fill="url(#g)" /> <animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" /> </svg>`; const toBase64 = (str: string) => typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str); export const dataUrl = `data:image/svg+xml;base64,${toBase64( shimmer(1000, 1000) )}`; // ==== End // FORM URL QUERY export const formUrlQuery = ({ searchParams, key, value, }: FormUrlQueryParams) => { const params = { ...qs.parse(searchParams.toString()), [key]: value }; return `${window.location.pathname}?${qs.stringify(params, { skipNulls: true, })}`; }; // REMOVE KEY FROM QUERY export function removeKeysFromQuery({ searchParams, keysToRemove, }: RemoveUrlQueryParams) { const currentUrl = qs.parse(searchParams); keysToRemove.forEach((key) => { delete currentUrl[key]; }); // Remove null or undefined values Object.keys(currentUrl).forEach( (key) => currentUrl[key] == null && delete currentUrl[key] ); return `${window.location.pathname}?${qs.stringify(currentUrl)}`; } // DEBOUNCE export const debounce = (func: (...args: any[]) => void, delay: number) => { let timeoutId: NodeJS.Timeout | null; return (...args: any[]) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); }; }; // GE IMAGE SIZE export type AspectRatioKey = keyof typeof aspectRatioOptions; export const getImageSize = ( type: string, image: any, dimension: "width" | "height" ): number => { if (type === "fill") { return ( aspectRatioOptions[image.aspectRatio as AspectRatioKey]?.[dimension] || 1000 ); } return image?.[dimension] || 1000; }; // DOWNLOAD IMAGE export const download = (url: string, filename: string) => { if (!url) { throw new Error("Resource URL not provided! You need to provide one"); } fetch(url) .then((response) => response.blob()) .then((blob) => { const blobURL = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobURL; if (filename && filename.length) a.download = `${filename.replace(" ", "_")}.png`; document.body.appendChild(a); a.click(); }) .catch((error) => console.log({ error })); }; // DEEP MERGE OBJECTS export const deepMergeObjects = (obj1: any, obj2: any) => { if(obj2 === null || obj2 === undefined) { return obj1; } let output = { ...obj2 }; for (let key in obj1) { if (obj1.hasOwnProperty(key)) { if ( obj1[key] && typeof obj1[key] === "object" && obj2[key] && typeof obj2[key] === "object" ) { output[key] = deepMergeObjects(obj1[key], obj2[key]); } else { output[key] = obj1[key]; } } } return output; }; ``` </details> <details> <summary><code>types/index.d.ts</code></summary> ```typescript /* eslint-disable no-unused-vars */ // ====== USER PARAMS declare type CreateUserParams = { clerkId: string; email: string; username: string; firstName: string; lastName: string; photo: string; }; declare type UpdateUserParams = { firstName: string; lastName: string; username: string; photo: string; }; // ====== IMAGE PARAMS declare type AddImageParams = { image: { title: string; publicId: string; transformationType: string; width: number; height: number; config: any; secureURL: string; transformationURL: string; aspectRatio: string | undefined; prompt: string | undefined; color: string | undefined; }; userId: string; path: string; }; declare type UpdateImageParams = { image: { _id: string; title: string; publicId: string; transformationType: string; width: number; height: number; config: any; secureURL: string; transformationURL: string; aspectRatio: string | undefined; prompt: string | undefined; color: string | undefined; }; userId: string; path: string; }; declare type Transformations = { restore?: boolean; fillBackground?: boolean; remove?: { prompt: string; removeShadow?: boolean; multiple?: boolean; }; recolor?: { prompt?: string; to: string; multiple?: boolean; }; removeBackground?: boolean; }; // ====== TRANSACTION PARAMS declare type CheckoutTransactionParams = { plan: string; credits: number; amount: number; buyerId: string; }; declare type CreateTransactionParams = { stripeId: string; amount: number; credits: number; plan: string; buyerId: string; createdAt: Date; }; declare type TransformationTypeKey = | "restore" | "fill" | "remove" | "recolor" | "removeBackground"; // ====== URL QUERY PARAMS declare type FormUrlQueryParams = { searchParams: string; key: string; value: string | number | null; }; declare type UrlQueryParams = { params: string; key: string; value: string | null; }; declare type RemoveUrlQueryParams = { searchParams: string; keysToRemove: string[]; }; declare type SearchParamProps = { params: { id: string; type: TransformationTypeKey }; searchParams: { [key: string]: string | string[] | undefined }; }; declare type TransformationFormProps = { action: "Add" | "Update"; userId: string; type: TransformationTypeKey; creditBalance: number; data?: IImage | null; config?: Transformations | null; }; declare type TransformedImageProps = { image: any; type: string; title: string; transformationConfig: Transformations | null; isTransforming: boolean; hasDownload?: boolean; setIsTransforming?: React.Dispatch<React.SetStateAction<boolean>>; }; ``` </details> <details> <summary><code>api/webhooks/clerk/route.ts</code></summary> ```typescript /* eslint-disable camelcase */ import { clerkClient } from "@clerk/nextjs"; import { WebhookEvent } from "@clerk/nextjs/server"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { Webhook } from "svix"; import { createUser, deleteUser, updateUser } from "@/lib/actions/user.actions"; export async function POST(req: Request) { // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; if (!WEBHOOK_SECRET) { throw new Error( "Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local" ); } // Get the headers const headerPayload = headers(); const svix_id = headerPayload.get("svix-id"); const svix_timestamp = headerPayload.get("svix-timestamp"); const svix_signature = headerPayload.get("svix-signature"); // If there are no headers, error out if (!svix_id || !svix_timestamp || !svix_signature) { return new Response("Error occured -- no svix headers", { status: 400, }); } // Get the body const payload = await req.json(); const body = JSON.stringify(payload); // Create a new Svix instance with your secret. const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent; // Verify the payload with the headers try { evt = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature, }) as WebhookEvent; } catch (err) { console.error("Error verifying webhook:", err); return new Response("Error occured", { status: 400, }); } // Get the ID and type const { id } = evt.data; const eventType = evt.type; // CREATE if (eventType === "user.created") { const { id, email_addresses, image_url, first_name, last_name, username } = evt.data; const user = { clerkId: id, email: email_addresses[0].email_address, username: username!, firstName: first_name, lastName: last_name, photo: image_url, }; const newUser = await createUser(user); // Set public metadata if (newUser) { await clerkClient.users.updateUserMetadata(id, { publicMetadata: { userId: newUser._id, }, }); } return NextResponse.json({ message: "OK", user: newUser }); } // UPDATE if (eventType === "user.updated") { const { id, image_url, first_name, last_name, username } = evt.data; const user = { firstName: first_name, lastName: last_name, username: username!, photo: image_url, }; const updatedUser = await updateUser(id, user); return NextResponse.json({ message: "OK", user: updatedUser }); } // DELETE if (eventType === "user.deleted") { const { id } = evt.data; const deletedUser = await deleteUser(id!); return NextResponse.json({ message: "OK", user: deletedUser }); } console.log(`Webhook with and ID of ${id} and type of ${eventType}`); console.log("Webhook body:", body); return new Response("", { status: 200 }); } ``` </details> <details> <summary><code>components/shared/CustomField.tsx</code></summary> ```typescript import React from "react"; import { Control } from "react-hook-form"; import { z } from "zod"; import { FormField, FormItem, FormControl, FormMessage, FormLabel, } from "../ui/form"; import { formSchema } from "./TransformationForm"; type CustomFieldProps = { control: Control<z.infer<typeof formSchema>> | undefined; render: (props: { field: any }) => React.ReactNode; name: keyof z.infer<typeof formSchema>; formLabel?: string; className?: string; }; export const CustomField = ({ control, render, name, formLabel, className, }: CustomFieldProps) => { return ( <FormField control={control} name={name} render={({ field }) => ( <FormItem className={className}> {formLabel && <FormLabel>{formLabel}</FormLabel>} <FormControl>{render({ field })}</FormControl> <FormMessage /> </FormItem> )} /> ); }; ``` </details> <details> <summary><code>components/shared/Collection.tsx</code></summary> ```typescript "use client"; import Image from "next/image"; import Link from "next/link"; import { useSearchParams, useRouter } from "next/navigation"; import { CldImage } from "next-cloudinary"; import { Pagination, PaginationContent, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { transformationTypes } from "@/constants"; import { IImage } from "@/lib/database/models/image.model"; import { formUrlQuery } from "@/lib/utils"; import { Button } from "../ui/button"; import { Search } from "./Search"; export const Collection = ({ hasSearch = false, images, totalPages = 1, page, }: { images: IImage[]; totalPages?: number; page: number; hasSearch?: boolean; }) => { const router = useRouter(); const searchParams = useSearchParams(); // PAGINATION HANDLER const onPageChange = (action: string) => { const pageValue = action === "next" ? Number(page) + 1 : Number(page) - 1; const newUrl = formUrlQuery({ searchParams: searchParams.toString(), key: "page", value: pageValue, }); router.push(newUrl, { scroll: false }); }; return ( <> <div className="collection-heading"> <h2 className="h2-bold text-dark-600">Recent Edits</h2> {hasSearch && <Search />} </div> {images.length > 0 ? ( <ul className="collection-list"> {images.map((image) => ( <Card image={image} key={image._id} /> ))} </ul> ) : ( <div className="collection-empty"> <p className="p-20-semibold">Empty List</p> </div> )} {totalPages > 1 && ( <Pagination className="mt-10"> <PaginationContent className="flex w-full"> <Button disabled={Number(page) <= 1} className="collection-btn" onClick={() => onPageChange("prev")} > <PaginationPrevious className="hover:bg-transparent hover:text-white" /> </Button> <p className="flex-center p-16-medium w-fit flex-1"> {page} / {totalPages} </p> <Button className="button w-32 bg-purple-gradient bg-cover text-white" onClick={() => onPageChange("next")} disabled={Number(page) >= totalPages} > <PaginationNext className="hover:bg-transparent hover:text-white" /> </Button> </PaginationContent> </Pagination> )} </> ); }; const Card = ({ image }: { image: IImage }) => { return ( <li> <Link href={`/transformations/${image._id}`} className="collection-card"> <CldImage src={image.publicId} alt={image.title} width={image.width} height={image.height} {...image.config} loading="lazy" className="h-52 w-full rounded-[10px] object-cover" sizes="(max-width: 767px) 100vw, (max-width: 1279px) 50vw, 33vw" /> <div className="flex-between"> <p className="p-20-semibold mr-3 line-clamp-1 text-dark-600"> {image.title} </p> <Image src={`/assets/icons/${ transformationTypes[ image.transformationType as TransformationTypeKey ].icon }`} alt={image.title} width={24} height={24} /> </div> </Link> </li> ); }; ``` </details> <details> <summary><code>components/shared/Search.tsx</code></summary> ```typescript "use client"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils"; export const Search = () => { const router = useRouter(); const searchParams = useSearchParams(); const [query, setQuery] = useState(""); useEffect(() => { const delayDebounceFn = setTimeout(() => { if (query) { const newUrl = formUrlQuery({ searchParams: searchParams.toString(), key: "query", value: query, }); router.push(newUrl, { scroll: false }); } else { const newUrl = removeKeysFromQuery({ searchParams: searchParams.toString(), keysToRemove: ["query"], }); router.push(newUrl, { scroll: false }); } }, 300); return () => clearTimeout(delayDebounceFn); }, [router, searchParams, query]); return ( <div className="search"> <Image src="/assets/icons/search.svg" alt="search" width={24} height={24} /> <Input className="search-field" placeholder="Search" onChange={(e) => setQuery(e.target.value)} /> </div> ); }; ``` </details> <details> <summary><code>image.actions.ts</code></summary> ```typescript "use server"; import { revalidatePath } from "next/cache"; import { connectToDatabase } from "../database/mongoose"; import { handleError } from "../utils"; import User from "../database/models/user.model"; import Image from "../database/models/image.model"; import { redirect } from "next/navigation"; import { v2 as cloudinary } from 'cloudinary' const populateUser = (query: any) => query.populate({ path: 'author', model: User, select: '_id firstName lastName clerkId' }) // ADD IMAGE export async function addImage({ image, userId, path }: AddImageParams) { try { await connectToDatabase(); const author = await User.findById(userId); if (!author) { throw new Error("User not found"); } const newImage = await Image.create({ ...image, author: author._id, }) revalidatePath(path); return JSON.parse(JSON.stringify(newImage)); } catch (error) { handleError(error) } } // UPDATE IMAGE export async function updateImage({ image, userId, path }: UpdateImageParams) { try { await connectToDatabase(); const imageToUpdate = await Image.findById(image._id); if (!imageToUpdate || imageToUpdate.author.toHexString() !== userId) { throw new Error("Unauthorized or image not found"); } const updatedImage = await Image.findByIdAndUpdate( imageToUpdate._id, image, { new: true } ) revalidatePath(path); return JSON.parse(JSON.stringify(updatedImage)); } catch (error) { handleError(error) } } // DELETE IMAGE export async function deleteImage(imageId: string) { try { await connectToDatabase(); await Image.findByIdAndDelete(imageId); } catch (error) { handleError(error) } finally{ redirect('/') } } // GET IMAGE export async function getImageById(imageId: string) { try { await connectToDatabase(); const image = await populateUser(Image.findById(imageId)); if(!image) throw new Error("Image not found"); return JSON.parse(JSON.stringify(image)); } catch (error) { handleError(error) } } // GET IMAGES export async function getAllImages({ limit = 9, page = 1, searchQuery = '' }: { limit?: number; page: number; searchQuery?: string; }) { try { await connectToDatabase(); cloudinary.config({ cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, secure: true, }) let expression = 'folder=imaginify'; if (searchQuery) { expression += ` AND ${searchQuery}` } const { resources } = await cloudinary.search .expression(expression) .execute(); const resourceIds = resources.map((resource: any) => resource.public_id); let query = {}; if(searchQuery) { query = { publicId: { $in: resourceIds } } } const skipAmount = (Number(page) -1) * limit; const images = await populateUser(Image.find(query)) .sort({ updatedAt: -1 }) .skip(skipAmount) .limit(limit); const totalImages = await Image.find(query).countDocuments(); const savedImages = await Image.find().countDocuments(); return { data: JSON.parse(JSON.stringify(images)), totalPage: Math.ceil(totalImages / limit), savedImages, } } catch (error) { handleError(error) } } // GET IMAGES BY USER export async function getUserImages({ limit = 9, page = 1, userId, }: { limit?: number; page: number; userId: string; }) { try { await connectToDatabase(); const skipAmount = (Number(page) - 1) * limit; const images = await populateUser(Image.find({ author: userId })) .sort({ updatedAt: -1 }) .skip(skipAmount) .limit(limit); const totalImages = await Image.find({ author: userId }).countDocuments(); return { data: JSON.parse(JSON.stringify(images)), totalPages: Math.ceil(totalImages / limit), }; } catch (error) { handleError(error); } } ``` </details> <details> <summary><code>transformations/[id]/page.tsx</code></summary> ```typescript import { auth } from "@clerk/nextjs"; import Image from "next/image"; import Link from "next/link"; import Header from "@/components/shared/Header"; import TransformedImage from "@/components/shared/TransformedImage"; import { Button } from "@/components/ui/button"; import { getImageById } from "@/lib/actions/image.actions"; import { getImageSize } from "@/lib/utils"; import { DeleteConfirmation } from "@/components/shared/DeleteConfirmation"; const ImageDetails = async ({ params: { id } }: SearchParamProps) => { const { userId } = auth(); const image = await getImageById(id); return ( <> <Header title={image.title} /> <section className="mt-5 flex flex-wrap gap-4"> <div className="p-14-medium md:p-16-medium flex gap-2"> <p className="text-dark-600">Transformation:</p> <p className=" capitalize text-purple-400"> {image.transformationType} </p> </div> {image.prompt && ( <> <p className="hidden text-dark-400/50 md:block">&#x25CF;</p> <div className="p-14-medium md:p-16-medium flex gap-2 "> <p className="text-dark-600">Prompt:</p> <p className=" capitalize text-purple-400">{image.prompt}</p> </div> </> )} {image.color && ( <> <p className="hidden text-dark-400/50 md:block">&#x25CF;</p> <div className="p-14-medium md:p-16-medium flex gap-2"> <p className="text-dark-600">Color:</p> <p className=" capitalize text-purple-400">{image.color}</p> </div> </> )} {image.aspectRatio && ( <> <p className="hidden text-dark-400/50 md:block">&#x25CF;</p> <div className="p-14-medium md:p-16-medium flex gap-2"> <p className="text-dark-600">Aspect Ratio:</p> <p className=" capitalize text-purple-400">{image.aspectRatio}</p> </div> </> )} </section> <section className="mt-10 border-t border-dark-400/15"> <div className="transformation-grid"> {/* MEDIA UPLOADER */} <div className="flex flex-col gap-4"> <h3 className="h3-bold text-dark-600">Original</h3> <Image width={getImageSize(image.transformationType, image, "width")} height={getImageSize(image.transformationType, image, "height")} src={image.secureURL} alt="image" className="transformation-original_image" /> </div> {/* TRANSFORMED IMAGE */} <TransformedImage image={image} type={image.transformationType} title={image.title} isTransforming={false} transformationConfig={image.config} hasDownload={true} /> </div> {userId === image.author.clerkId && ( <div className="mt-4 space-y-4"> <Button asChild type="button" className="submit-button capitalize"> <Link href={`/transformations/${image._id}/update`}> Update Image </Link> </Button> <DeleteConfirmation imageId={image._id} /> </div> )} </section> </> ); }; export default ImageDetails; ``` </details> <details> <summary><code>transformations/[id]/update/page.tsx</code></summary> ```typescript import { auth } from "@clerk/nextjs"; import { redirect } from "next/navigation"; import Header from "@/components/shared/Header"; import TransformationForm from "@/components/shared/TransformationForm"; import { transformationTypes } from "@/constants"; import { getUserById } from "@/lib/actions/user.actions"; import { getImageById } from "@/lib/actions/image.actions"; const Page = async ({ params: { id } }: SearchParamProps) => { const { userId } = auth(); if (!userId) redirect("/sign-in"); const user = await getUserById(userId); const image = await getImageById(id); const transformation = transformationTypes[image.transformationType as TransformationTypeKey]; return ( <> <Header title={transformation.title} subtitle={transformation.subTitle} /> <section className="mt-10"> <TransformationForm action="Update" userId={user._id} type={image.transformationType as TransformationTypeKey} creditBalance={user.creditBalance} config={image.config} data={image} /> </section> </> ); }; export default Page; ``` </details> <details> <summary><code>components/shared/DeleteConfirmation.tsx</code></summary> ```typescript "use client"; import { useTransition } from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { deleteImage } from "@/lib/actions/image.actions"; import { Button } from "../ui/button"; export const DeleteConfirmation = ({ imageId }: { imageId: string }) => { const [isPending, startTransition] = useTransition(); return ( <AlertDialog> <AlertDialogTrigger asChild className="w-full rounded-full"> <Button type="button" className="button h-[44px] w-full md:h-[54px]" variant="destructive" > Delete Image </Button> </AlertDialogTrigger> <AlertDialogContent className="flex flex-col gap-10"> <AlertDialogHeader> <AlertDialogTitle> Are you sure you want to delete this image? </AlertDialogTitle> <AlertDialogDescription className="p-16-regular"> This will permanently delete this image </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction className="border bg-red-500 text-white hover:bg-red-600" onClick={() => startTransition(async () => { await deleteImage(imageId); }) } > {isPending ? "Deleting..." : "Delete"} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); }; ``` </details> <details> <summary><code>api/webhooks/stripe/route.ts</code></summary> ```typescript /* eslint-disable camelcase */ import { createTransaction } from "@/lib/actions/transaction.action"; import { NextResponse } from "next/server"; import stripe from "stripe"; export async function POST(request: Request) { const body = await request.text(); const sig = request.headers.get("stripe-signature") as string; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!; let event; try { event = stripe.webhooks.constructEvent(body, sig, endpointSecret); } catch (err) { return NextResponse.json({ message: "Webhook error", error: err }); } // Get the ID and type const eventType = event.type; // CREATE if (eventType === "checkout.session.completed") { const { id, amount_total, metadata } = event.data.object; const transaction = { stripeId: id, amount: amount_total ? amount_total / 100 : 0, plan: metadata?.plan || "", credits: Number(metadata?.credits) || 0, buyerId: metadata?.buyerId || "", createdAt: new Date(), }; const newTransaction = await createTransaction(transaction); return NextResponse.json({ message: "OK", transaction: newTransaction }); } return new Response("", { status: 200 }); } ``` </details> <details> <summary><code>credits/page.tsx</code></summary> ```typescript import { SignedIn, auth } from "@clerk/nextjs"; import Image from "next/image"; import { redirect } from "next/navigation"; import Header from "@/components/shared/Header"; import { Button } from "@/components/ui/button"; import { plans } from "@/constants"; import { getUserById } from "@/lib/actions/user.actions"; import Checkout from "@/components/shared/Checkout"; const Credits = async () => { const { userId } = auth(); if (!userId) redirect("/sign-in"); const user = await getUserById(userId); return ( <> <Header title="Buy Credits" subtitle="Choose a credit package that suits your needs!" /> <section> <ul className="credits-list"> {plans.map((plan) => ( <li key={plan.name} className="credits-item"> <div className="flex-center flex-col gap-3"> <Image src={plan.icon} alt="check" width={50} height={50} /> <p className="p-20-semibold mt-2 text-purple-500"> {plan.name} </p> <p className="h1-semibold text-dark-600">${plan.price}</p> <p className="p-16-regular">{plan.credits} Credits</p> </div> {/* Inclusions */} <ul className="flex flex-col gap-5 py-9"> {plan.inclusions.map((inclusion) => ( <li key={plan.name + inclusion.label} className="flex items-center gap-4" > <Image src={`/assets/icons/${ inclusion.isIncluded ? "check.svg" : "cross.svg" }`} alt="check" width={24} height={24} /> <p className="p-16-regular">{inclusion.label}</p> </li> ))} </ul> {plan.name === "Free" ? ( <Button variant="outline" className="credits-btn"> Free Consumable </Button> ) : ( <SignedIn> <Checkout plan={plan.name} amount={plan.price} credits={plan.credits} buyerId={user._id} /> </SignedIn> )} </li> ))} </ul> </section> </> ); }; export default Credits; ``` </details> <details> <summary><code>components/shared/Checkout.tsx</code></summary> ```typescript "use client"; import { loadStripe } from "@stripe/stripe-js"; import { useEffect } from "react"; import { useToast } from "@/components/ui/use-toast"; import { checkoutCredits } from "@/lib/actions/transaction.action"; import { Button } from "../ui/button"; const Checkout = ({ plan, amount, credits, buyerId, }: { plan: string; amount: number; credits: number; buyerId: string; }) => { const { toast } = useToast(); useEffect(() => { loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); }, []); useEffect(() => { // Check to see if this is a redirect back from Checkout const query = new URLSearchParams(window.location.search); if (query.get("success")) { toast({ title: "Order placed!", description: "You will receive an email confirmation", duration: 5000, className: "success-toast", }); } if (query.get("canceled")) { toast({ title: "Order canceled!", description: "Continue to shop around and checkout when you're ready", duration: 5000, className: "error-toast", }); } }, []); const onCheckout = async () => { const transaction = { plan, amount, credits, buyerId, }; await checkoutCredits(transaction); }; return ( <form action={onCheckout} method="POST"> <section> <Button type="submit" role="link" className="w-full rounded-full bg-purple-gradient bg-cover" > Buy Credit </Button> </section> </form> ); }; export default Checkout; ``` </details> <details> <summary><code>profile/page.tsx</code></summary> ```typescript import { auth } from "@clerk/nextjs"; import Image from "next/image"; import { redirect } from "next/navigation"; import { Collection } from "@/components/shared/Collection"; import Header from "@/components/shared/Header"; import { getUserImages } from "@/lib/actions/image.actions"; import { getUserById } from "@/lib/actions/user.actions"; const Profile = async ({ searchParams }: SearchParamProps) => { const page = Number(searchParams?.page) || 1; const { userId } = auth(); if (!userId) redirect("/sign-in"); const user = await getUserById(userId); const images = await getUserImages({ page, userId: user._id }); return ( <> <Header title="Profile" /> <section className="profile"> <div className="profile-balance"> <p className="p-14-medium md:p-16-medium">CREDITS AVAILABLE</p> <div className="mt-4 flex items-center gap-4"> <Image src="/assets/icons/coins.svg" alt="coins" width={50} height={50} className="size-9 md:size-12" /> <h2 className="h2-bold text-dark-600">{user.creditBalance}</h2> </div> </div> <div className="profile-image-manipulation"> <p className="p-14-medium md:p-16-medium">IMAGE MANIPULATION DONE</p> <div className="mt-4 flex items-center gap-4"> <Image src="/assets/icons/photo.svg" alt="coins" width={50} height={50} className="size-9 md:size-12" /> <h2 className="h2-bold text-dark-600">{images?.data.length}</h2> </div> </div> </section> <section className="mt-8 md:mt-14"> <Collection images={images?.data} totalPages={images?.totalPages} page={page} /> </section> </> ); }; export default Profile; ``` </details> ## <a name="links">πŸ”— Links</a> Public Assets used in the project can be found [here](https://drive.google.com/file/d/1uv1zyCjbYBQE9qnwh2snwO0NBgoop5gz/view?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

AI Tools Digital Product Sales
1.4K Github Stars
projects_realestate
Open Source

projects_realestate

# Real Estate App ![Real Estate](https://i.ibb.co/jTW4bFC/image.png) ### [🌟 Become a top 1% Next.js 13 developer in only one course](https://jsmastery.pro/next13) ### [πŸš€ Land your dream programming job in 6 months](https://jsmastery.pro/masterclass)

Frontend Templates Mobile Development
1.3K Github Stars
zoom-clone
Open Source

zoom-clone

<div align="center"> <br /> <a href="https://youtu.be/R8CIO1DZ2b8" target="_blank"> <img src="https://github.com/adrianhajdin/zoom-clone/assets/67959015/f09a8421-67d3-45ce-b9bc-a791cdc2774b" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> </div> <h3 align="center">A Zoom Clone</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Assets & Code](#snippets) 6. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/R8CIO1DZ2b8" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Built with the latest Next.js and TypeScript, this project replicates Zoom, a widely used video conferencing tool. It enables users to securely log in, create meetings and access various meeting functionalities such as recording, screen sharing, and managing participants. If you're getting started and need assistance or face any bugs, join our active Discord community with over 30 thousand members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - TypeScript - Clerk - getstream - shadcn - Tailwind CSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Authentication**: Implements authentication and authorization features using Clerk, allowing users to securely log in via social sign-on or traditional email and password methods, while ensuring appropriate access levels and permissions within the platform. πŸ‘‰ **New Meeting**: Quickly start a new meeting, configuring camera and microphone settings before joining. πŸ‘‰ **Meeting Controls**: Participants have full control over meeting aspects, including recording, emoji reactions, screen sharing, muting/unmuting, sound adjustments, grid layout, participant list view, and individual participant management (pinning, muting, unmuting, blocking, allowing video share). πŸ‘‰ **Exit Meeting**: Participants can leave a meeting, or creators can end it for all attendees. πŸ‘‰ **Schedule Future Meetings**: Input meeting details (date, time) to schedule future meetings, accessible on the 'Upcoming Meetings' page for sharing the link or immediate start. πŸ‘‰ **Past Meetings List**: Access a list of previously held meetings, including details and metadata. πŸ‘‰ **View Recorded Meetings**: Access recordings of past meetings for review or reference. πŸ‘‰ **Personal Room**: Users have a personal room with a unique meeting link for instant meetings, shareable with others. πŸ‘‰ **Join Meetings via Link**: Easily join meetings created by others by providing a link. πŸ‘‰ **Secure Real-time Functionality**: All interactions within the platform are secure and occur in real-time, maintaining user privacy and data integrity. πŸ‘‰ **Responsive Design**: Follows responsive design principles to ensure optimal user experience across devices, adapting seamlessly to different screen sizes and resolutions. and many more, including code architecture and reusability. ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/zoom-clone.git cd zoom-clone ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env` in the root of your project and add the following content: ```env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_STREAM_API_KEY= STREAM_SECRET_KEY= ``` Replace the placeholder values with your actual Clerk & getstream credentials. You can obtain these credentials by signing up on the [Clerk website](https://clerk.com/) and [getstream website](https://getstream.io/) **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>app/globals.css</code></summary> ```css @tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; } /* ======== stream css overrides ======== */ .str-video__call-stats { max-width: 500px; position: relative; } .str-video__speaker-layout__wrapper { max-height: 700px; } .str-video__participant-details { color: white; } .str-video__menu-container { color: white; } .str-video__notification { color: white; } .str-video__participant-list { background-color: #1c1f2e; padding: 10px; border-radius: 10px; color: white; height: 100%; } .str-video__call-controls__button { height: 40px; } .glassmorphism { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .glassmorphism2 { background: rgba(18, 17, 17, 0.25); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } /* ==== clerk class override ===== */ .cl-userButtonPopoverActionButtonIcon { color: white; } .cl-logoBox { height: 40px; } .cl-dividerLine { background: #252a41; height: 2px; } .cl-socialButtonsIconButton { border: 3px solid #565761; } .cl-internal-wkkub3 { color: white; } .cl-userButtonPopoverActionButton { color: white; } /* =============================== */ @layer utilities { .flex-center { @apply flex justify-center items-center; } .flex-between { @apply flex justify-between items-center; } } /* animation */ .show-block { width: 100%; max-width: 350px; display: block; animation: show 0.7s forwards linear; } @keyframes show { 0% { animation-timing-function: ease-in; width: 0%; } 100% { animation-timing-function: ease-in; width: 100%; } } ``` </details> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript import type { Config } from 'tailwindcss'; const config = { darkMode: ['class'], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], prefix: '', theme: { container: { center: true, padding: '2rem', screens: { '2xl': '1400px', }, }, extend: { colors: { dark: { 1: '#1C1F2E', 2: '#161925', 3: '#252A41', 4: '#1E2757', }, blue: { 1: '#0E78F9', }, sky: { 1: '#C9DDFF', 2: '#ECF0FF', 3: '#F5FCFF', }, orange: { 1: '#FF742E', }, purple: { 1: '#830EF9', }, yellow: { 1: '#F9A90E', }, }, keyframes: { 'accordion-down': { from: { height: '0' }, to: { height: 'var(--radix-accordion-content-height)' }, }, 'accordion-up': { from: { height: 'var(--radix-accordion-content-height)' }, to: { height: '0' }, }, }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', }, backgroundImage: { hero: "url('/images/hero-background.png')", }, }, }, plugins: [require('tailwindcss-animate')], } satisfies Config; export default config; ``` </details> <details> <summary><code>components/MeetingCard.tsx</code></summary> ```typescript "use client"; import Image from "next/image"; import { cn } from "@/lib/utils"; import { Button } from "./ui/button"; import { avatarImages } from "@/constants"; import { useToast } from "./ui/use-toast"; interface MeetingCardProps { title: string; date: string; icon: string; isPreviousMeeting?: boolean; buttonIcon1?: string; buttonText?: string; handleClick: () => void; link: string; } const MeetingCard = ({ icon, title, date, isPreviousMeeting, buttonIcon1, handleClick, link, buttonText, }: MeetingCardProps) => { const { toast } = useToast(); return ( <section className="flex min-h-[258px] w-full flex-col justify-between rounded-[14px] bg-dark-1 px-5 py-8 xl:max-w-[568px]"> <article className="flex flex-col gap-5"> <Image src={icon} alt="upcoming" width={28} height={28} /> <div className="flex justify-between"> <div className="flex flex-col gap-2"> <h1 className="text-2xl font-bold">{title}</h1> <p className="text-base font-normal">{date}</p> </div> </div> </article> <article className={cn("flex justify-center relative", {})}> <div className="relative flex w-full max-sm:hidden"> {avatarImages.map((img, index) => ( <Image key={index} src={img} alt="attendees" width={40} height={40} className={cn("rounded-full", { absolute: index > 0 })} style={{ top: 0, left: index * 28 }} /> ))} <div className="flex-center absolute left-[136px] size-10 rounded-full border-[5px] border-dark-3 bg-dark-4"> +5 </div> </div> {!isPreviousMeeting && ( <div className="flex gap-2"> <Button onClick={handleClick} className="rounded bg-blue-1 px-6"> {buttonIcon1 && ( <Image src={buttonIcon1} alt="feature" width={20} height={20} /> )} &nbsp; {buttonText} </Button> <Button onClick={() => { navigator.clipboard.writeText(link); toast({ title: "Link Copied", }); }} className="bg-dark-4 px-6" > <Image src="/icons/copy.svg" alt="feature" width={20} height={20} /> &nbsp; Copy Link </Button> </div> )} </article> </section> ); }; export default MeetingCard; ``` </details> ## <a name="links">πŸ”— Links</a> Public assets used in the project can be found [here](https://drive.google.com/file/d/1ofTpWii_sCIdJ14uQ431xWVXpYgjtQ8Q/view?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning experience. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://www.jsmastery.pro/ultimate-next-course" target="_blank"> <img src="https://i.ibb.co/804sPK6/Image-720.png" alt="Project Banner"> </a>

Project Management Video Conferencing
1.2K Github Stars
yc_directory
Open Source

yc_directory

<div align="center"> <br /> <a href="https://youtu.be/Zq5fmkH0T78?feature=shared" target="_blank"> <img src="https://github.com/user-attachments/assets/471e2baa-8781-43b8-aaed-62e313d03e99" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Typescript-black?style=for-the-badge&logoColor=white&logo=react&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> <img src="https://img.shields.io/badge/-Sanity-black?style=for-the-badge&logoColor=white&logo=sanity&color=F03E2F" alt="sanity" /> </div> <h3 align="center">Startup Directory Platform</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets (Code to Copy)](#snippets) 6. πŸ”— [Assets](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/Zq5fmkH0T78?feature=shared" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> A Next.js 15 platform where entrepreneurs can submit their startup ideas for virtual pitch competitions, browse other pitches, and gain exposure through a clean minimalistic design for a smooth user experience. If you're getting started and need assistance or face any bugs, join our active Discord community with over **34k+** members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - React 19 - Next.js 15 - Sanity - TailwindCSS - ShadCN - TypeScript ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Live Content API**: Displays the latest startup ideas dynamically on the homepage using Sanity's Content API. πŸ‘‰ **GitHub Authentication**: Allows users to log in easily using their GitHub account. πŸ‘‰ **Pitch Submission**: Users can submit startup ideas, including title, description, category, and multimedia links ( image or video). πŸ‘‰ **View Pitches**: Browse through submitted ideas with filtering options by category. πŸ‘‰ **Pitch Details Page**: Click on any pitch to view its details, with multimedia and description displayed. πŸ‘‰ **Profile Page**: Users can view the list of pitches they've submitted. πŸ‘‰ **Editor Picks**: Admins can highlight top startup ideas using the "Editor Picks" feature managed via Sanity Studio. πŸ‘‰ **Views Counter**: Tracks the number of views for each pitch instead of an upvote system. πŸ‘‰ **Search**: Search functionality to load and view pitches efficiently. πŸ‘‰ **Minimalistic Design**: Fresh and simple UI with only the essential pages for ease of use and a clean aesthetic. and many more, including the latest **React 19**, **Next.js 15** and **Sanity** features alongside code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/JavaScript-Mastery-Pro/pitchify.git cd pitchify ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env.local` in the root of your project and add the following content: ```env NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= NEXT_PUBLIC_SANITY_API_VERSION='vX' SANITY_TOKEN= AUTH_SECRET= AUTH_GITHUB_ID= AUTH_GITHUB_SECRET= ``` Replace the placeholder values with your actual Sanity credentials. You can obtain these credentials by signing up & creating a new project on the [Sanity website](https://www.sanity.io/). **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript import type {Config} from "tailwindcss"; const config: Config = { darkMode: ["class"], content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", "./sanity/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { screens: { xs: "475px", }, colors: { primary: { "100": "#FFE8F0", DEFAULT: "#EE2B69", }, secondary: "#FBE843", black: { "100": "#333333", "200": "#141413", "300": "#7D8087", DEFAULT: "#000000", }, white: { "100": "#F7F7F7", DEFAULT: "#FFFFFF", }, }, fontFamily: { "work-sans": ["var(--font-work-sans)"], }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, boxShadow: { 100: "2px 2px 0px 0px rgb(0, 0, 0)", 200: "2px 2px 0px 2px rgb(0, 0, 0)", 300: "2px 2px 0px 2px rgb(238, 43, 105)", }, }, }, plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], }; export default config; ``` </details> <details> <summary><code>globals.css</code></summary> ```css @import url("https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --radius: 0.5rem; } } @layer utilities { .flex-between { @apply flex justify-between items-center; } .text-30-extrabold { @apply text-[30px] font-extrabold text-white; } .text-30-bold { @apply text-[30px] font-bold text-black; } .text-30-semibold { @apply font-semibold text-[30px] text-black; } .text-26-semibold { @apply font-semibold text-[26px] text-black; } .text-24-black { @apply text-[24px] font-black text-black; } .text-20-medium { @apply font-medium text-[20px] text-black; } .text-16-medium { @apply font-medium text-[16px] text-black; } .text-14-normal { @apply font-normal text-sm text-white-100/80; } .pink_container { @apply w-full bg-primary min-h-[530px] pattern flex justify-center items-center flex-col py-10 px-6; } .tag { @apply bg-secondary px-6 py-3 font-work-sans font-bold rounded-sm uppercase relative tag-tri; } .heading { @apply uppercase bg-black px-6 py-3 font-work-sans font-extrabold text-white sm:text-[54px] sm:leading-[64px] text-[36px] leading-[46px] max-w-5xl text-center my-5; } .sub-heading { @apply font-medium text-[20px] text-white max-w-2xl text-center break-words; } .section_container { @apply px-6 py-10 max-w-7xl mx-auto; } .card_grid { @apply grid md:grid-cols-3 sm:grid-cols-2 gap-5; } .card_grid-sm { @apply grid sm:grid-cols-2 gap-5; } .no-result { @apply text-black-100 text-sm font-normal; } /* profile */ .profile_container { @apply w-full pb-10 pt-20 px-6 max-w-7xl mx-auto lg:flex-row flex-col flex gap-10; } .profile_card { @apply w-80 px-6 pb-6 pt-20 flex flex-col justify-center items-center bg-primary border-[5px] border-black shadow-100 rounded-[30px] relative z-0 h-fit max-lg:w-full; } .profile_title { @apply w-11/12 bg-white border-[5px] border-black rounded-[20px] px-5 py-3 absolute -top-9 after:absolute after:content-[''] after:-top-1 after:right-0 after:-skew-y-6 after:bg-black after:-z-[1] after:rounded-[20px] after:w-full after:h-[60px] before:absolute before:content-[''] before:-bottom-1 before:left-0 before:-skew-y-6 before:w-full before:h-[60px] before:bg-black before:-z-[1] before:rounded-[20px] shadow-100; } .profile_image { @apply rounded-full object-cover border-[3px] border-black; } /* idea details */ .divider { @apply border-dotted bg-zinc-400 max-w-4xl my-10 mx-auto; } .view_skeleton { @apply bg-zinc-400 h-10 w-24 rounded-lg fixed bottom-3 right-3; } /* navbar */ .avatar { @apply p-0 focus-visible:ring-0 bg-none rounded-full drop-shadow-md !important; } .dropdown-menu { @apply w-56 border-[5px] border-black bg-white p-5 rounded-2xl !important; } .login { @apply border-[5px] py-4 border-black bg-white text-black relative shadow-100 font-work-sans font-medium hover:shadow-none transition-all duration-500 !important; } /* searchform */ .search-form { @apply max-w-3xl w-full min-h-[80px] bg-white border-[5px] border-black rounded-[80px] text-[24px] mt-8 px-5 flex flex-row items-center gap-5; } .search-input { @apply flex-1 font-bold placeholder:font-semibold placeholder:text-black-100 w-full h-auto outline-none; } .search-btn { @apply size-[50px] rounded-full bg-black flex justify-center items-center !important; } /* startupcard */ .startup-card { @apply bg-white border-[5px] border-black py-6 px-5 rounded-[22px] shadow-200 hover:border-primary transition-all duration-500 hover:shadow-300 hover:bg-primary-100; } .startup-card_date { @apply font-medium text-[16px] bg-primary-100 px-4 py-2 rounded-full group-hover:bg-white-100; } .startup-card_desc { @apply font-normal text-[16px] line-clamp-2 my-3 text-black-100 break-all; } .startup-card_img { @apply w-full h-[164px] rounded-[10px] object-cover; } .startup-card_btn { @apply rounded-full bg-black-200 font-medium text-[16px] text-white px-5 py-3 !important; } .startup-card_skeleton { @apply w-full h-96 rounded-[22px] bg-zinc-400; } /* startupform */ .startup-form { @apply max-w-2xl mx-auto bg-white my-10 space-y-8 px-6; } .startup-form_label { @apply font-bold text-[18px] text-black uppercase; } .startup-form_input { @apply border-[3px] border-black px-5 py-7 text-[18px] text-black font-semibold rounded-full mt-3 placeholder:text-black-300 !important; } .startup-form_textarea { @apply border-[3px] border-black p-5 text-[18px] text-black font-semibold rounded-[20px] mt-3 placeholder:text-black-300 !important; } .startup-form_error { @apply text-red-500 mt-2 ml-5; } .startup-form_editor { @apply mt-3 border-[3px] border-black text-[18px] text-black font-semibold placeholder:text-black-300 !important; } .startup-form_btn { @apply bg-primary border-[4px] border-black rounded-full p-5 min-h-[70px] w-full font-bold text-[18px] !important; } /* view */ .view-container { @apply flex justify-end items-center mt-5 fixed bottom-3 right-3; } .view-text { @apply font-medium text-[16px] bg-primary-100 px-4 py-2 rounded-lg capitalize; } .category-tag { @apply font-medium text-[16px] bg-primary-100 px-4 py-2 rounded-full; } .pattern { background-image: linear-gradient( to right, transparent 49.5%, rgba(251, 232, 67, 0.2) 49.5%, rgba(251, 232, 67, 0.6) 50.5%, transparent 50.5% ); background-size: 5% 100%; background-position: center; background-repeat: repeat-x; } .tag-tri { @apply before:content-[''] before:absolute before:top-2 before:left-2 before:border-t-[10px] before:border-t-black before:border-r-[10px] before:border-r-transparent after:content-[''] after:absolute after:bottom-2 after:right-2 after:border-b-[10px] after:border-b-black after:border-l-[10px] after:border-l-transparent; } } .w-md-editor-toolbar { padding: 10px !important; } ``` </details> <details> <summary><code>lib/utils.ts</code></summary> ```typescript import {clsx, type ClassValue} from "clsx"; import {twMerge} from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function parseServerActionResponse<T>(response: T) { return JSON.parse(JSON.stringify(response)); } export function formatDate(date: string) { return new Date(date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", }); } export function formatNumber(number: number) { if (number >= 1000000) { return (number / 1000000).toFixed(1).replace(/\.0$/, "") + "M"; // Convert to millions } else if (number >= 1000) { return (number / 1000).toFixed(1).replace(/\.0$/, "") + "k"; // Convert to thousands } else { return number.toString(); // Return the number as is if below 1000 } } ``` </details> <details> <summary><code>lib/validation.ts</code></summary> ```typescript import {z} from "zod"; export const formSchema = z.object({ title: z.string().min(3, "Title is required").max(100, "Title is too long"), description: z .string() .min(20, "Description should be at least 20 characters") .max(500, "Description is too long. Max 500 characters at most"), category: z .string() .min(3, "Category should be at least 3 characters") .max(20, "Category is too long. Max 20 characters at most"), link: z .string() .url("Invalid Image URL") .refine(async (url) => { try { const res = await fetch(url, {method: "HEAD"}); const contentType = res.headers.get("content-type"); return contentType?.startsWith("image/"); } catch { return false; } }, "URL must be a valid image"), pitch: z.string().min(10, "Pitch should be at least 10 characters"), }); ``` </details> <details> <summary><code>components/Ping.tsx</code></summary> ```typescript jsx const Ping = () => { return ( <div className="relative"> <div className="absolute -left-4 top-1"> <span className="flex size-[11px]"> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75"></span> <span className="relative inline-flex size-[11px] rounded-full bg-primary"></span> </span> </div> </div> ); }; export default Ping; ``` </details> <details> <summary><code>PLAYLIST_BY_SLUG_QUERY</code></summary> ```typescript export const PLAYLIST_BY_SLUG_QUERY = defineQuery(`*[_type == "playlist" && slug.current == $slug][0]{ _id, title, slug, select[]->{ _id, _createdAt, title, slug, author->{ _id, name, slug, image, bio }, views, description, category, image, pitch } }`); ``` </details> ## <a name="links">πŸ”— Assets</a> - Fonts and Assets used in the project can be found [here](https://drive.google.com/file/d/1OEFHnEq5pQFP86u8FOBLBBNxKPsbjjqU/view?usp=sharing) - [Learn Server Actions](https://youtu.be/FKZAXFjxlJI?feature=shared) - [Applicaton Workflow](https://miro.com/app/board/uXjVLT_tMdU=/?share_link_id=580854757703) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning experience. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://www.jsmastery.pro/ultimate-next-course" target="_blank"> <img src="https://i.ibb.co/804sPK6/Image-720.png" alt="Project Banner"> </a>

Web Development Frontend Templates
945 Github Stars
podcastr
Open Source

podcastr

<div align="center"> <br /> <a href="https://youtu.be/zfAb95tJvZQ" target="_blank"> <img src="https://github.com/adrianhajdin/jsm_podcastr/assets/151519281/f61a58c2-f144-41f7-8bc9-5ad14752ceb3" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-Typescript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6" alt="typescript" /> <img src="https://img.shields.io/badge/-Next_._JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000" alt="nextdotjs" /> <img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" /> <img src="https://img.shields.io/badge/-OpenAI-black?style=for-the-badge&logoColor=white&logo=openai&color=412991" alt="openai" /> </div> <h3 align="center">AI Podcast Platform</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets (Code to Copy)](#snippets) 6. πŸ”— [Assets](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/zfAb95tJvZQ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> A cutting-edge AI SaaS platform that enables users to create, discover, and enjoy podcasts with advanced features like text-to-audio conversion with multi-voice AI, podcast thumbnail Image generation and seamless playback. If you're getting started and need assistance or face any bugs, join our active Discord community with over **34k+** members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - Next.js - TypeScript - Convex - OpenAI - Clerk - ShadCN - Tailwind CSS ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Robust Authentication**: Secure and reliable user login and registration system. πŸ‘‰ **Modern Home Page**: Showcases trending podcasts with a sticky podcast player for continuous listening. πŸ‘‰ **Discover Podcasts Page**: Dedicated page for users to explore new and popular podcasts. πŸ‘‰ **Fully Functional Search**: Allows users to find podcasts easily using various search criteria. πŸ‘‰ **Create Podcast Page**: Enables podcast creation with text-to-audio conversion, AI image generation, and previews. πŸ‘‰ **Multi Voice AI Functionality**: Supports multiple AI-generated voices for dynamic podcast creation. πŸ‘‰ **Profile Page**: View all created podcasts with options to delete them. πŸ‘‰ **Podcast Details Page**: Displays detailed information about each podcast, including creator details, number of listeners, and transcript. πŸ‘‰ **Podcast Player**: Features backward/forward controls, as well as mute/unmute functionality for a seamless listening experience. πŸ‘‰ **Responsive Design**: Fully functional and visually appealing across all devices and screen sizes. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/jsm_podcastr.git cd jsm_podcastr ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Set Up Environment Variables** Create a new file named `.env` in the root of your project and add the following content: ```env CONVEX_DEPLOYMENT= NEXT_PUBLIC_CONVEX_URL= NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= NEXT_PUBLIC_CLERK_SIGN_IN_URL='/sign-in' NEXT_PUBLIC_CLERK_SIGN_UP_URL='/sign-up' ``` Replace the placeholder values with your actual Convex & Clerk credentials. You can obtain these credentials by signing up on the [Convex](https://www.convex.dev/) and [Clerk](https://clerk.com/) websites. **Running the Project** ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>app/globals.css</code></summary> ```css @tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; } html { background-color: #101114; } @layer utilities { .input-class { @apply text-16 placeholder:text-16 bg-black-1 rounded-[6px] placeholder:text-gray-1 border-none text-gray-1; } .podcast_grid { @apply grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4; } .right_sidebar { @apply sticky right-0 top-0 flex w-[310px] flex-col overflow-y-hidden border-none bg-black-1 px-[30px] pt-8 max-xl:hidden; } .left_sidebar { @apply sticky left-0 top-0 flex w-fit flex-col justify-between border-none bg-black-1 pt-8 text-white-1 max-md:hidden lg:w-[270px] lg:pl-8; } .generate_thumbnail { @apply mt-[30px] flex w-full max-w-[520px] flex-col justify-between gap-2 rounded-lg border border-black-6 bg-black-1 px-2.5 py-2 md:flex-row md:gap-0; } .image_div { @apply flex-center mt-5 h-[142px] w-full cursor-pointer flex-col gap-3 rounded-xl border-[3.2px] border-dashed border-black-6 bg-black-1; } .carousel_box { @apply relative flex h-fit aspect-square w-full flex-none cursor-pointer flex-col justify-end rounded-xl border-none; } .button_bold-16 { @apply text-[16px] font-bold text-white-1 transition-all duration-500; } .flex-center { @apply flex items-center justify-center; } .text-12 { @apply text-[12px] leading-normal; } .text-14 { @apply text-[14px] leading-normal; } .text-16 { @apply text-[16px] leading-normal; } .text-18 { @apply text-[18px] leading-normal; } .text-20 { @apply text-[20px] leading-normal; } .text-24 { @apply text-[24px] leading-normal; } .text-32 { @apply text-[32px] leading-normal; } } /* ===== custom classes ===== */ .custom-scrollbar::-webkit-scrollbar { width: 3px; height: 3px; border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-track { background: #15171c; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #222429; border-radius: 50px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #555; } /* Hide scrollbar for Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE, Edge and Firefox */ .no-scrollbar { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } .glassmorphism { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .glassmorphism-auth { background: rgba(6, 3, 3, 0.711); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .glassmorphism-black { background: rgba(18, 18, 18, 0.64); backdrop-filter: blur(37px); -webkit-backdrop-filter: blur(37px); } /* ======= clerk overrides ======== */ .cl-socialButtonsIconButton { border: 2px solid #222429; } .cl-button { color: white; } .cl-socialButtonsProviderIcon__github { filter: invert(1); } .cl-internal-b3fm6y { background: #f97535; } .cl-formButtonPrimary { background: #f97535; } .cl-footerActionLink { color: #f97535; } .cl-headerSubtitle { color: #c5d0e6; } .cl-logoImage { width: 10rem; height: 3rem; } .cl-internal-4a7e9l { color: white; } .cl-userButtonPopoverActionButtonIcon { color: white; } .cl-internal-wkkub3 { color: #f97535; } ``` </details> <details> <summary><code>tailwind.config.ts</code></summary> ```typescript import type { Config } from "tailwindcss"; const config = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { white: { 1: "#FFFFFF", 2: "rgba(255, 255, 255, 0.72)", 3: "rgba(255, 255, 255, 0.4)", 4: "rgba(255, 255, 255, 0.64)", 5: "rgba(255, 255, 255, 0.80)", }, black: { 1: "#15171C", 2: "#222429", 3: "#101114", 4: "#252525", 5: "#2E3036", 6: "#24272C", }, orange: { 1: "#F97535", }, gray: { 1: "#71788B", }, }, backgroundImage: { "nav-focus": "linear-gradient(270deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.00) 100%)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config; export default config; ``` </details> <details> <summary><code>constants/index.ts</code></summary> ```typescript export const sidebarLinks = [ { imgURL: "/icons/home.svg", route: "/", label: "Home", }, { imgURL: "/icons/discover.svg", route: "/discover", label: "Discover", }, { imgURL: "/icons/microphone.svg", route: "/create-podcast", label: "Create Podcast", }, ]; export const voiceDetails = [ { id: 1, name: "alloy", }, { id: 2, name: "echo", }, { id: 3, name: "fable", }, { id: 4, name: "onyx", }, { id: 5, name: "nova", }, { id: 6, name: "shimmer", }, ]; export const podcastData = [ { id: 1, title: "The Joe Rogan Experience", description: "A long form, in-depth conversation", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/3106b884-548d-4ba0-a179-785901f69806", }, { id: 2, title: "The Futur", description: "This is how the news should sound", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/16fbf9bd-d800-42bc-ac95-d5a586447bf6", }, { id: 3, title: "Waveform", description: "Join Michelle Obama in conversation", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/60f0c1d9-f2ac-4a96-9178-f01d78fa3733", }, { id: 4, title: "The Tech Talks Daily Podcast", description: "This is how the news should sound", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/5ba7ed1b-88b4-4c32-8d71-270f1c502445", }, { id: 5, title: "GaryVee Audio Experience", description: "A long form, in-depth conversation", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/ca7cb1a6-4919-4b2c-a73e-279a79ac6d23", }, { id: 6, title: "Syntax ", description: "Join Michelle Obama in conversation", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/b8ea40c7-aafb-401a-9129-73c515a73ab5", }, { id: 7, title: "IMPAULSIVE", description: "A long form, in-depth conversation", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/8a55d662-fe3f-4bcf-b78b-3b2f3d3def5c", }, { id: 8, title: "Ted Tech", description: "This is how the news should sound", imgURL: "https://lovely-flamingo-139.convex.cloud/api/storage/221ee4bd-435f-42c3-8e98-4a001e0d806e", }, ]; ``` </details> <details> <summary><code>convex/http.ts</code></summary> ```typescript // ===== reference links ===== // https://www.convex.dev/templates (open the link and choose for clerk than you will get the github link mentioned below) // https://github.dev/webdevcody/thumbnail-critique/blob/6637671d72513cfe13d00cb7a2990b23801eb327/convex/schema.ts import type { WebhookEvent } from "@clerk/nextjs/server"; import { httpRouter } from "convex/server"; import { Webhook } from "svix"; import { internal } from "./_generated/api"; import { httpAction } from "./_generated/server"; const handleClerkWebhook = httpAction(async (ctx, request) => { const event = await validateRequest(request); if (!event) { return new Response("Invalid request", { status: 400 }); } switch (event.type) { case "user.created": await ctx.runMutation(internal.users.createUser, { clerkId: event.data.id, email: event.data.email_addresses[0].email_address, imageUrl: event.data.image_url, name: event.data.first_name as string, }); break; case "user.updated": await ctx.runMutation(internal.users.updateUser, { clerkId: event.data.id, imageUrl: event.data.image_url, email: event.data.email_addresses[0].email_address, }); break; case "user.deleted": await ctx.runMutation(internal.users.deleteUser, { clerkId: event.data.id as string, }); break; } return new Response(null, { status: 200, }); }); const http = httpRouter(); http.route({ path: "/clerk", method: "POST", handler: handleClerkWebhook, }); const validateRequest = async ( req: Request ): Promise<WebhookEvent | undefined> => { // key note : add the webhook secret variable to the environment variables field in convex dashboard setting const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!; if (!webhookSecret) { throw new Error("CLERK_WEBHOOK_SECRET is not defined"); } const payloadString = await req.text(); const headerPayload = req.headers; const svixHeaders = { "svix-id": headerPayload.get("svix-id")!, "svix-timestamp": headerPayload.get("svix-timestamp")!, "svix-signature": headerPayload.get("svix-signature")!, }; const wh = new Webhook(webhookSecret); const event = wh.verify(payloadString, svixHeaders); return event as unknown as WebhookEvent; }; export default http; ``` </details> <details> <summary><code>convex/users.ts</code></summary> ```typescript import { ConvexError, v } from "convex/values"; import { internalMutation, query } from "./_generated/server"; export const getUserById = query({ args: { clerkId: v.string() }, handler: async (ctx, args) => { const user = await ctx.db .query("users") .filter((q) => q.eq(q.field("clerkId"), args.clerkId)) .unique(); if (!user) { throw new ConvexError("User not found"); } return user; }, }); // this query is used to get the top user by podcast count. first the podcast is sorted by views and then the user is sorted by total podcasts, so the user with the most podcasts will be at the top. export const getTopUserByPodcastCount = query({ args: {}, handler: async (ctx, args) => { const user = await ctx.db.query("users").collect(); const userData = await Promise.all( user.map(async (u) => { const podcasts = await ctx.db .query("podcasts") .filter((q) => q.eq(q.field("authorId"), u.clerkId)) .collect(); const sortedPodcasts = podcasts.sort((a, b) => b.views - a.views); return { ...u, totalPodcasts: podcasts.length, podcast: sortedPodcasts.map((p) => ({ podcastTitle: p.podcastTitle, pocastId: p._id, })), }; }) ); return userData.sort((a, b) => b.totalPodcasts - a.totalPodcasts); }, }); export const createUser = internalMutation({ args: { clerkId: v.string(), email: v.string(), imageUrl: v.string(), name: v.string(), }, handler: async (ctx, args) => { await ctx.db.insert("users", { clerkId: args.clerkId, email: args.email, imageUrl: args.imageUrl, name: args.name, }); }, }); export const updateUser = internalMutation({ args: { clerkId: v.string(), imageUrl: v.string(), email: v.string(), }, async handler(ctx, args) { const user = await ctx.db .query("users") .filter((q) => q.eq(q.field("clerkId"), args.clerkId)) .unique(); if (!user) { throw new ConvexError("User not found"); } await ctx.db.patch(user._id, { imageUrl: args.imageUrl, email: args.email, }); const podcast = await ctx.db .query("podcasts") .filter((q) => q.eq(q.field("authorId"), args.clerkId)) .collect(); await Promise.all( podcast.map(async (p) => { await ctx.db.patch(p._id, { authorImageUrl: args.imageUrl, }); }) ); }, }); export const deleteUser = internalMutation({ args: { clerkId: v.string() }, async handler(ctx, args) { const user = await ctx.db .query("users") .filter((q) => q.eq(q.field("clerkId"), args.clerkId)) .unique(); if (!user) { throw new ConvexError("User not found"); } await ctx.db.delete(user._id); }, }); ``` </details> <details> <summary><code>types/index.ts</code></summary> ```typescript /* eslint-disable no-unused-vars */ import { Dispatch, SetStateAction } from "react"; import { Id } from "@/convex/_generated/dataModel"; export interface EmptyStateProps { title: string; search?: boolean; buttonText?: string; buttonLink?: string; } export interface TopPodcastersProps { _id: Id<"users">; _creationTime: number; email: string; imageUrl: string; clerkId: string; name: string; podcast: { podcastTitle: string; pocastId: Id<"podcasts">; }[]; totalPodcasts: number; } export interface PodcastProps { _id: Id<"podcasts">; _creationTime: number; audioStorageId: Id<"_storage"> | null; user: Id<"users">; podcastTitle: string; podcastDescription: string; audioUrl: string | null; imageUrl: string | null; imageStorageId: Id<"_storage"> | null; author: string; authorId: string; authorImageUrl: string; voicePrompt: string; imagePrompt: string | null; voiceType: string; audioDuration: number; views: number; } export interface ProfilePodcastProps { podcasts: PodcastProps[]; listeners: number; } export type VoiceType = | "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer"; export interface GeneratePodcastProps { voiceType: VoiceType; setAudio: Dispatch<SetStateAction<string>>; audio: string; setAudioStorageId: Dispatch<SetStateAction<Id<"_storage"> | null>>; voicePrompt: string; setVoicePrompt: Dispatch<SetStateAction<string>>; setAudioDuration: Dispatch<SetStateAction<number>>; } export interface GenerateThumbnailProps { setImage: Dispatch<SetStateAction<string>>; setImageStorageId: Dispatch<SetStateAction<Id<"_storage"> | null>>; image: string; imagePrompt: string; setImagePrompt: Dispatch<SetStateAction<string>>; } export interface LatestPodcastCardProps { imgUrl: string; title: string; duration: string; index: number; audioUrl: string; author: string; views: number; podcastId: Id<"podcasts">; } export interface PodcastDetailPlayerProps { audioUrl: string; podcastTitle: string; author: string; isOwner: boolean; imageUrl: string; podcastId: Id<"podcasts">; imageStorageId: Id<"_storage">; audioStorageId: Id<"_storage">; authorImageUrl: string; authorId: string; } export interface AudioProps { title: string; audioUrl: string; author: string; imageUrl: string; podcastId: string; } export interface AudioContextType { audio: AudioProps | undefined; setAudio: React.Dispatch<React.SetStateAction<AudioProps | undefined>>; } export interface PodcastCardProps { imgUrl: string; title: string; description: string; podcastId: Id<"podcasts">; } export interface CarouselProps { fansLikeDetail: TopPodcastersProps[]; } export interface ProfileCardProps { podcastData: ProfilePodcastProps; imageUrl: string; userFirstName: string; } export type UseDotButtonType = { selectedIndex: number; scrollSnaps: number[]; onDotButtonClick: (index: number) => void; }; ``` </details> <details> <summary><code>convex/podcasts.ts</code></summary> ```typescript import { ConvexError, v } from "convex/values"; import { mutation, query } from "./_generated/server"; // create podcast mutation export const createPodcast = mutation({ args: { audioStorageId: v.union(v.id("_storage"), v.null()), podcastTitle: v.string(), podcastDescription: v.string(), audioUrl: v.string(), imageUrl: v.string(), imageStorageId: v.union(v.id("_storage"), v.null()), voicePrompt: v.string(), imagePrompt: v.string(), voiceType: v.string(), views: v.number(), audioDuration: v.number(), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new ConvexError("User not authenticated"); } const user = await ctx.db .query("users") .filter((q) => q.eq(q.field("email"), identity.email)) .collect(); if (user.length === 0) { throw new ConvexError("User not found"); } return await ctx.db.insert("podcasts", { audioStorageId: args.audioStorageId, user: user[0]._id, podcastTitle: args.podcastTitle, podcastDescription: args.podcastDescription, audioUrl: args.audioUrl, imageUrl: args.imageUrl, imageStorageId: args.imageStorageId, author: user[0].name, authorId: user[0].clerkId, voicePrompt: args.voicePrompt, imagePrompt: args.imagePrompt, voiceType: args.voiceType, views: args.views, authorImageUrl: user[0].imageUrl, audioDuration: args.audioDuration, }); }, }); // this mutation is required to generate the url after uploading the file to the storage. export const getUrl = mutation({ args: { storageId: v.id("_storage"), }, handler: async (ctx, args) => { return await ctx.storage.getUrl(args.storageId); }, }); // this query will get all the podcasts based on the voiceType of the podcast , which we are showing in the Similar Podcasts section. export const getPodcastByVoiceType = query({ args: { podcastId: v.id("podcasts"), }, handler: async (ctx, args) => { const podcast = await ctx.db.get(args.podcastId); return await ctx.db .query("podcasts") .filter((q) => q.and( q.eq(q.field("voiceType"), podcast?.voiceType), q.neq(q.field("_id"), args.podcastId) ) ) .collect(); }, }); // this query will get all the podcasts. export const getAllPodcasts = query({ handler: async (ctx) => { return await ctx.db.query("podcasts").order("desc").collect(); }, }); // this query will get the podcast by the podcastId. export const getPodcastById = query({ args: { podcastId: v.id("podcasts"), }, handler: async (ctx, args) => { return await ctx.db.get(args.podcastId); }, }); // this query will get the podcasts based on the views of the podcast , which we are showing in the Trending Podcasts section. export const getTrendingPodcasts = query({ handler: async (ctx) => { const podcast = await ctx.db.query("podcasts").collect(); return podcast.sort((a, b) => b.views - a.views).slice(0, 8); }, }); // this query will get the podcast by the authorId. export const getPodcastByAuthorId = query({ args: { authorId: v.string(), }, handler: async (ctx, args) => { const podcasts = await ctx.db .query("podcasts") .filter((q) => q.eq(q.field("authorId"), args.authorId)) .collect(); const totalListeners = podcasts.reduce( (sum, podcast) => sum + podcast.views, 0 ); return { podcasts, listeners: totalListeners }; }, }); // this query will get the podcast by the search query. export const getPodcastBySearch = query({ args: { search: v.string(), }, handler: async (ctx, args) => { if (args.search === "") { return await ctx.db.query("podcasts").order("desc").collect(); } const authorSearch = await ctx.db .query("podcasts") .withSearchIndex("search_author", (q) => q.search("author", args.search)) .take(10); if (authorSearch.length > 0) { return authorSearch; } const titleSearch = await ctx.db .query("podcasts") .withSearchIndex("search_title", (q) => q.search("podcastTitle", args.search) ) .take(10); if (titleSearch.length > 0) { return titleSearch; } return await ctx.db .query("podcasts") .withSearchIndex("search_body", (q) => q.search("podcastDescription" || "podcastTitle", args.search) ) .take(10); }, }); // this mutation will update the views of the podcast. export const updatePodcastViews = mutation({ args: { podcastId: v.id("podcasts"), }, handler: async (ctx, args) => { const podcast = await ctx.db.get(args.podcastId); if (!podcast) { throw new ConvexError("Podcast not found"); } return await ctx.db.patch(args.podcastId, { views: podcast.views + 1, }); }, }); // this mutation will delete the podcast. export const deletePodcast = mutation({ args: { podcastId: v.id("podcasts"), imageStorageId: v.id("_storage"), audioStorageId: v.id("_storage"), }, handler: async (ctx, args) => { const podcast = await ctx.db.get(args.podcastId); if (!podcast) { throw new ConvexError("Podcast not found"); } await ctx.storage.delete(args.imageStorageId); await ctx.storage.delete(args.audioStorageId); return await ctx.db.delete(args.podcastId); }, }); ``` </details> <details> <summary><code>components/PodcastDetailPlayer.ts</code></summary> ```typescript "use client"; import { useMutation } from "convex/react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { api } from "@/convex/_generated/api"; import { useAudio } from "@/providers/AudioProvider"; import { PodcastDetailPlayerProps } from "@/types"; import LoaderSpinner from "./Loader"; import { Button } from "./ui/button"; import { useToast } from "./ui/use-toast"; const PodcastDetailPlayer = ({ audioUrl, podcastTitle, author, imageUrl, podcastId, imageStorageId, audioStorageId, isOwner, authorImageUrl, authorId, }: PodcastDetailPlayerProps) => { const router = useRouter(); const { setAudio } = useAudio(); const { toast } = useToast(); const [isDeleting, setIsDeleting] = useState(false); const deletePodcast = useMutation(api.podcasts.deletePodcast); const handleDelete = async () => { try { await deletePodcast({ podcastId, imageStorageId, audioStorageId }); toast({ title: "Podcast deleted", }); router.push("/"); } catch (error) { console.error("Error deleting podcast", error); toast({ title: "Error deleting podcast", variant: "destructive", }); } }; const handlePlay = () => { setAudio({ title: podcastTitle, audioUrl, imageUrl, author, podcastId, }); }; if (!imageUrl || !authorImageUrl) return <LoaderSpinner />; return ( <div className="mt-6 flex w-full justify-between max-md:justify-center"> <div className="flex flex-col gap-8 max-md:items-center md:flex-row"> <Image src={imageUrl} width={250} height={250} alt="Podcast image" className="aspect-square rounded-lg" /> <div className="flex w-full flex-col gap-5 max-md:items-center md:gap-9"> <article className="flex flex-col gap-2 max-md:items-center"> <h1 className="text-32 font-extrabold tracking-[-0.32px] text-white-1"> {podcastTitle} </h1> <figure className="flex cursor-pointer items-center gap-2" onClick={() => { router.push(`/profile/${authorId}`); }} > <Image src={authorImageUrl} width={30} height={30} alt="Caster icon" className="size-[30px] rounded-full object-cover" /> <h2 className="text-16 font-normal text-white-3">{author}</h2> </figure> </article> <Button onClick={handlePlay} className="text-16 w-full max-w-[250px] bg-orange-1 font-extrabold text-white-1" > <Image src="/icons/Play.svg" width={20} height={20} alt="random play" />{" "} &nbsp; Play podcast </Button> </div> </div> {isOwner && ( <div className="relative mt-2"> <Image src="/icons/three-dots.svg" width={20} height={30} alt="Three dots icon" className="cursor-pointer" onClick={() => setIsDeleting((prev) => !prev)} /> {isDeleting && ( <div className="absolute -left-32 -top-2 z-10 flex w-32 cursor-pointer justify-center gap-2 rounded-md bg-black-6 py-1.5 hover:bg-black-2" onClick={handleDelete} > <Image src="/icons/delete.svg" width={16} height={16} alt="Delete icon" /> <h2 className="text-16 font-normal text-white-1">Delete</h2> </div> )} </div> )} </div> ); }; export default PodcastDetailPlayer; ``` </details> <details> <summary><code>components/PodcastPlayer.ts</code></summary> ```typescript "use client"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { formatTime } from "@/lib/formatTime"; import { cn } from "@/lib/utils"; import { useAudio } from "@/providers/AudioProvider"; import { Progress } from "./ui/progress"; const PodcastPlayer = () => { const audioRef = useRef<HTMLAudioElement>(null); const [isPlaying, setIsPlaying] = useState(false); const [duration, setDuration] = useState(0); const [isMuted, setIsMuted] = useState(false); const [currentTime, setCurrentTime] = useState(0); const { audio } = useAudio(); const togglePlayPause = () => { if (audioRef.current?.paused) { audioRef.current?.play(); setIsPlaying(true); } else { audioRef.current?.pause(); setIsPlaying(false); } }; const toggleMute = () => { if (audioRef.current) { audioRef.current.muted = !isMuted; setIsMuted((prev) => !prev); } }; const forward = () => { if ( audioRef.current && audioRef.current.currentTime && audioRef.current.duration && audioRef.current.currentTime + 5 < audioRef.current.duration ) { audioRef.current.currentTime += 5; } }; const rewind = () => { if (audioRef.current && audioRef.current.currentTime - 5 > 0) { audioRef.current.currentTime -= 5; } else if (audioRef.current) { audioRef.current.currentTime = 0; } }; useEffect(() => { const updateCurrentTime = () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); } }; const audioElement = audioRef.current; if (audioElement) { audioElement.addEventListener("timeupdate", updateCurrentTime); return () => { audioElement.removeEventListener("timeupdate", updateCurrentTime); }; } }, []); useEffect(() => { const audioElement = audioRef.current; if (audio?.audioUrl) { if (audioElement) { audioElement.play().then(() => { setIsPlaying(true); }); } } else { audioElement?.pause(); setIsPlaying(true); } }, [audio]); const handleLoadedMetadata = () => { if (audioRef.current) { setDuration(audioRef.current.duration); } }; const handleAudioEnded = () => { setIsPlaying(false); }; return ( <div className={cn("sticky bottom-0 left-0 flex size-full flex-col", { hidden: !audio?.audioUrl || audio?.audioUrl === "", })} > {/* change the color for indicator inside the Progress component in ui folder */} <Progress value={(currentTime / duration) * 100} className="w-full" max={duration} /> <section className="glassmorphism-black flex h-[112px] w-full items-center justify-between px-4 max-md:justify-center max-md:gap-5 md:px-12"> <audio ref={audioRef} src={audio?.audioUrl} className="hidden" onLoadedMetadata={handleLoadedMetadata} onEnded={handleAudioEnded} /> <div className="flex items-center gap-4 max-md:hidden"> <Link href={`/podcast/${audio?.podcastId}`}> <Image src={audio?.imageUrl! || "/images/player1.png"} width={64} height={64} alt="player1" className="aspect-square rounded-xl" /> </Link> <div className="flex w-[160px] flex-col"> <h2 className="text-14 truncate font-semibold text-white-1"> {audio?.title} </h2> <p className="text-12 font-normal text-white-2">{audio?.author}</p> </div> </div> <div className="flex-center cursor-pointer gap-3 md:gap-6"> <div className="flex items-center gap-1.5"> <Image src={"/icons/reverse.svg"} width={24} height={24} alt="rewind" onClick={rewind} /> <h2 className="text-12 font-bold text-white-4">-5</h2> </div> <Image src={isPlaying ? "/icons/Pause.svg" : "/icons/Play.svg"} width={30} height={30} alt="play" onClick={togglePlayPause} /> <div className="flex items-center gap-1.5"> <h2 className="text-12 font-bold text-white-4">+5</h2> <Image src={"/icons/forward.svg"} width={24} height={24} alt="forward" onClick={forward} /> </div> </div> <div className="flex items-center gap-6"> <h2 className="text-16 font-normal text-white-2 max-md:hidden"> {formatTime(duration)} </h2> <div className="flex w-full gap-2"> <Image src={isMuted ? "/icons/unmute.svg" : "/icons/mute.svg"} width={24} height={24} alt="mute unmute" onClick={toggleMute} className="cursor-pointer" /> </div> </div> </section> </div> ); }; export default PodcastPlayer; ``` </details> <details> <summary><code>lib/formatTime.ts</code></summary> ```typescript export const formatTime = (seconds: number) => { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds < 10 ? "0" : ""}${remainingSeconds}`; }; ``` </details> <details> <summary><code>lib/useDebounce.ts</code></summary> ```typescript import { useEffect, useState } from "react"; export const useDebounce = <T>(value: T, delay = 500) => { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { const timeout = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timeout); }; }, [value, delay]); return debouncedValue; }; ``` </details> <details> <summary><code>(root)/profile/[profiled]/page.tsx</code></summary> ```typescript "use client"; import { useQuery } from "convex/react"; import EmptyState from "@/components/EmptyState"; import LoaderSpinner from "@/components/Loader"; import PodcastCard from "@/components/PodcastCard"; import ProfileCard from "@/components/ProfileCard"; import { api } from "@/convex/_generated/api"; const ProfilePage = ({ params, }: { params: { profileId: string; }; }) => { const user = useQuery(api.users.getUserById, { clerkId: params.profileId, }); const podcastsData = useQuery(api.podcasts.getPodcastByAuthorId, { authorId: params.profileId, }); if (!user || !podcastsData) return <LoaderSpinner />; return ( <section className="mt-9 flex flex-col"> <h1 className="text-20 font-bold text-white-1 max-md:text-center"> Podcaster Profile </h1> <div className="mt-6 flex flex-col gap-6 max-md:items-center md:flex-row"> <ProfileCard podcastData={podcastsData!} imageUrl={user?.imageUrl!} userFirstName={user?.name!} /> </div> <section className="mt-9 flex flex-col gap-5"> <h1 className="text-20 font-bold text-white-1">All Podcasts</h1> {podcastsData && podcastsData.podcasts.length > 0 ? ( <div className="podcast_grid"> {podcastsData?.podcasts ?.slice(0, 4) .map((podcast) => ( <PodcastCard key={podcast._id} imgUrl={podcast.imageUrl!} title={podcast.podcastTitle!} description={podcast.podcastDescription} podcastId={podcast._id} /> ))} </div> ) : ( <EmptyState title="You have not created any podcasts yet" buttonLink="/create-podcast" /> )} </section> </section> ); }; export default ProfilePage; ``` </details> <details> <summary><code>componenets/ProfileCard.tsx</code></summary> ```typescript "use client"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useAudio } from "@/providers/AudioProvider"; import { PodcastProps, ProfileCardProps } from "@/types"; import LoaderSpinner from "./Loader"; import { Button } from "./ui/button"; const ProfileCard = ({ podcastData, imageUrl, userFirstName, }: ProfileCardProps) => { const { setAudio } = useAudio(); const [randomPodcast, setRandomPodcast] = useState<PodcastProps | null>(null); const playRandomPodcast = () => { const randomIndex = Math.floor(Math.random() * podcastData.podcasts.length); setRandomPodcast(podcastData.podcasts[randomIndex]); }; useEffect(() => { if (randomPodcast) { setAudio({ title: randomPodcast.podcastTitle, audioUrl: randomPodcast.audioUrl || "", imageUrl: randomPodcast.imageUrl || "", author: randomPodcast.author, podcastId: randomPodcast._id, }); } }, [randomPodcast, setAudio]); if (!imageUrl) return <LoaderSpinner />; return ( <div className="mt-6 flex flex-col gap-6 max-md:items-center md:flex-row"> <Image src={imageUrl} width={250} height={250} alt="Podcaster" className="aspect-square rounded-lg" /> <div className="flex flex-col justify-center max-md:items-center"> <div className="flex flex-col gap-2.5"> <figure className="flex gap-2 max-md:justify-center"> <Image src="/icons/verified.svg" width={15} height={15} alt="verified" /> <h2 className="text-14 font-medium text-white-2"> Verified Creator </h2> </figure> <h1 className="text-32 font-extrabold tracking-[-0.32px] text-white-1"> {userFirstName} </h1> </div> <figure className="flex gap-3 py-6"> <Image src="/icons/headphone.svg" width={24} height={24} alt="headphones" /> <h2 className="text-16 font-semibold text-white-1"> {podcastData?.listeners} &nbsp; <span className="font-normal text-white-2">monthly listeners</span> </h2> </figure> {podcastData?.podcasts.length > 0 && ( <Button onClick={playRandomPodcast} className="text-16 bg-orange-1 font-extrabold text-white-1" > <Image src="/icons/Play.svg" width={20} height={20} alt="random play" />{" "} &nbsp; Play a random podcast </Button> )} </div> </div> ); }; export default ProfileCard; ``` </details> ## <a name="links">πŸ”— Assets</a> Public assets used in the project can be found [here](https://drive.google.com/file/d/18tLuq1QY1Wxr4sqnMony2LCLDcyYCWdG/view?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Podcast Tools Video Streaming & Transcoding
775 Github Stars
aora
Open Source

aora

<div align="center"> <br /> <a href="https://youtu.be/ZBCUegTZF7M?si=ubt0vk70lSjt6DCs" target="_blank"> <img src="https://i.postimg.cc/5NR9bxFM/Sora-README.png" alt="Project Banner"> </a> <br /> <div> <img src="https://img.shields.io/badge/-React_Native-black?style=for-the-badge&logoColor=white&logo=react&color=61DAFB" alt="react.js" /> <img src="https://img.shields.io/badge/-Appwrite-black?style=for-the-badge&logoColor=white&logo=appwrite&color=FD366E" alt="appwrite" /> <img src="https://img.shields.io/badge/NativeWind-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="nativewind" /> </div> <h3 align="center">Video Sharing App</h3> <div align="center"> Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family! </div> </div> ## πŸ“‹ <a name="table">Table of Contents</a> 1. πŸ€– [Introduction](#introduction) 2. βš™οΈ [Tech Stack](#tech-stack) 3. πŸ”‹ [Features](#features) 4. 🀸 [Quick Start](#quick-start) 5. πŸ•ΈοΈ [Snippets](#snippets) 6. πŸ”— [Links](#links) 7. πŸš€ [More](#more) ## 🚨 Tutorial This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a>. If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! <a href="https://youtu.be/ZBCUegTZF7M?si=ubt0vk70lSjt6DCs" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d" /></a> ## <a name="introduction">πŸ€– Introduction</a> Built with React Native for seamless user experiences, Animatable for captivating animations, and integrated with the dependable backend systems of Appwrite, this app showcases impressive design and functionality, enabling seamless sharing of AI videos within the community. If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out. <a href="https://discord.com/invite/n6EdbFJ" target="_blank"><img src="https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e" /></a> ## <a name="tech-stack">βš™οΈ Tech Stack</a> - React Native - Expo - Nativewind - Animatable - Appwrite ## <a name="features">πŸ”‹ Features</a> πŸ‘‰ **Onboarding Screen**: Engaging graphics and clear instructions welcome users to the app. πŸ‘‰ **Robust Authentication & Authorization System**: Secure email login safeguards user accounts. πŸ‘‰ **Dynamic Home Screen with Animated Flat List**: Smoothly animated flat list showcases the latest videos for seamless browsing. πŸ‘‰ **Pull-to-Refresh Functionality**: Users can refresh content with a simple pull gesture for up-to-date information. πŸ‘‰ **Full-Text Search Capability**: Efficiently search through videos with real-time suggestions and instant results. πŸ‘‰ **Tab Navigation**: Navigate between sections like Home, Search, and Profile with ease using tab navigation. πŸ‘‰ **Post Creation Screen for Uploading Media**: Upload video and image posts directly from the app with integrated media selection. πŸ‘‰ **Profile Screen with Detailed Insights**: View account details and activity, including uploaded videos and follower count, for a personalized experience. πŸ‘‰ **Responsiveness**: Smooth performance and adaptability across various devices and screen sizes for a consistent user experience. πŸ‘‰ **Animations**: Dynamic animations using the Animatable library to enhance user interaction and engagement throughout the app's UI. and many more, including code architecture and reusability ## <a name="quick-start">🀸 Quick Start</a> Follow these steps to set up the project locally on your machine. **Prerequisites** Make sure you have the following installed on your machine: - [Git](https://git-scm.com/) - [Node.js](https://nodejs.org/en) - [npm](https://www.npmjs.com/) (Node Package Manager) **Cloning the Repository** ```bash git clone https://github.com/adrianhajdin/aora.git cd aora ``` **Installation** Install the project dependencies using npm: ```bash npm install ``` **Running the Project** ```bash npm start ``` **Expo Go** Download the [Expo Go](https://expo.dev/go) app onto your device, then use it to scan the QR code from Terminal and run. ## <a name="snippets">πŸ•ΈοΈ Snippets</a> <details> <summary><code>tailwind.config.js</code></summary> ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], theme: { extend: { colors: { primary: "#161622", secondary: { DEFAULT: "#FF9C01", 100: "#FF9001", 200: "#FF8E01", }, black: { DEFAULT: "#000", 100: "#1E1E2D", 200: "#232533", }, gray: { 100: "#CDCDE0", }, }, fontFamily: { pthin: ["Poppins-Thin", "sans-serif"], pextralight: ["Poppins-ExtraLight", "sans-serif"], plight: ["Poppins-Light", "sans-serif"], pregular: ["Poppins-Regular", "sans-serif"], pmedium: ["Poppins-Medium", "sans-serif"], psemibold: ["Poppins-SemiBold", "sans-serif"], pbold: ["Poppins-Bold", "sans-serif"], pextrabold: ["Poppins-ExtraBold", "sans-serif"], pblack: ["Poppins-Black", "sans-serif"], }, }, }, plugins: [], }; ``` </details> <details> <summary><code>Font Loaded</code></summary> ```javascript const [fontsLoaded, error] = useFonts({ "Poppins-Black": require("../assets/fonts/Poppins-Black.ttf"), "Poppins-Bold": require("../assets/fonts/Poppins-Bold.ttf"), "Poppins-ExtraBold": require("../assets/fonts/Poppins-ExtraBold.ttf"), "Poppins-ExtraLight": require("../assets/fonts/Poppins-ExtraLight.ttf"), "Poppins-Light": require("../assets/fonts/Poppins-Light.ttf"), "Poppins-Medium": require("../assets/fonts/Poppins-Medium.ttf"), "Poppins-Regular": require("../assets/fonts/Poppins-Regular.ttf"), "Poppins-SemiBold": require("../assets/fonts/Poppins-SemiBold.ttf"), "Poppins-Thin": require("../assets/fonts/Poppins-Thin.ttf"), }); useEffect(() => { if (error) throw error; if (fontsLoaded) { SplashScreen.hideAsync(); } }, [fontsLoaded, error]); if (!fontsLoaded && !error) { return null; } ``` </details> <details> <summary><code>Dummy Videos for Appwrite</code></summary> ```javascript const videos = [ { title: "Get inspired to code", thumbnail: "https://i.ibb.co/tJBcX20/Appwrite-video.png", video: "https://player.vimeo.com/video/949579770?h=897cd5e781", prompt: "Create a motivating AI driven video aimed at inspiring coding enthusiasts with simple language", }, { title: "How AI Shapes Coding Future", thumbnail: "https://i.ibb.co/Xkgk7DY/Video.png", video: "https://player.vimeo.com/video/949581999?h=4672125b31", prompt: "Picture the future of coding with AI. Show AR VR", }, { title: "Dalmatian's journey through Italy", thumbnail: "https://i.ibb.co/CBYzyKh/Video-1.png", video: "https://player.vimeo.com/video/949582778?h=d60220d68d", prompt: "Create a heartwarming video following the travels of dalmatian dog exploring beautiful Italy", }, { title: "Meet small AI friends", thumbnail: "https://i.ibb.co/7XqVPVT/Photo-1677756119517.png", video: "https://player.vimeo.com/video/949616422?h=d60220d68d", prompt: "Make a video about a small blue AI robot blinking its eyes and looking at the screen", }, { title: "Find inspiration in Every Line", thumbnail: "https://i.ibb.co/mGfCYJY/Video-2.png", video: "https://player.vimeo.com/video/949617485?h=d60220d68d", prompt: "A buy working on his laptop that sparks excitement for coding, emphasizing the endless possibilities and personal growth it offers", }, { title: "Japan's Blossoming temple", thumbnail: "https://i.ibb.co/3Y2Nk7q/Bucket-215.png", video: "https://player.vimeo.com/video/949618057?h=d60220d68d", prompt: "Create a captivating video journey through Japan's Sakura Temple", }, { title: "A Glimpse into Tomorrow's VR World", thumbnail: "https://i.ibb.co/C5wXXf9/Video-3.png", video: "https://player.vimeo.com/video/949620017?h=d60220d68d", prompt: "An imaginative video envisioning the future of Virtual Reality", }, { title: "A World where Ideas Grow Big", thumbnail: "https://i.ibb.co/DzXRfyr/Bucket-59038.png", video: "https://player.vimeo.com/video/949620200?h=d60220d68d", prompt: "Make a fun video about hackers and all the cool stuff they do with computers", }, ]; ``` </details> ## <a name="links">πŸ”— Links</a> Assets and constants used in the project can be found [here](https://drive.google.com/drive/folders/1pckq7VAoqZlmsEfYaSsDltmQSESKm8h7?usp=sharing) ## <a name="more">πŸš€ More</a> **Advance your skills with Next.js 14 Pro Course** Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go! <a href="https://jsmastery.pro/next14" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71" alt="Project Banner"> </a> <br /> <br /> **Accelerate your professional journey with the Expert Training program** And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together! <a href="https://www.jsmastery.pro/masterclass" target="_blank"> <img src="https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84" alt="Project Banner"> </a> #

Frontend Templates Mobile Development
2.4K Github Stars