├── .eslintrc.json ├── public ├── logo.png ├── vercel.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── addtopics │ │ └── page.tsx │ ├── published │ │ └── [storyId] │ │ │ └── page.tsx │ ├── p │ │ └── [storyId] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── login │ │ └── page.tsx │ └── globals.css ├── interfaces │ └── index.ts ├── lib │ ├── utils.ts │ ├── style.css │ ├── AuthProvider.tsx │ ├── auth.ts │ ├── cloudinary.ts │ └── data.ts ├── db │ ├── drizzle.ts │ └── schema.ts ├── components │ ├── GetStories.tsx │ ├── Separator.tsx │ ├── Sidebar.tsx │ ├── SidebarStories.tsx │ ├── ui │ │ ├── input.tsx │ │ ├── toaster.tsx │ │ ├── badge.tsx │ │ ├── avatar.tsx │ │ ├── use-toast.ts │ │ └── toast.tsx │ ├── Ads.tsx │ ├── UserBadget.tsx │ ├── Favorite.tsx │ ├── ImageComp.tsx │ ├── Share.tsx │ ├── ReplyComponent.tsx │ ├── AddTags.tsx │ ├── CommentArea.tsx │ ├── GetComments.tsx │ ├── NavbarStory.tsx │ ├── Topics.tsx │ ├── Header.tsx │ ├── StoryDetail.tsx │ ├── CodeBlock.tsx │ ├── UserEngagement.tsx │ ├── StoryRender.tsx │ ├── StoryTags.tsx │ ├── CommentsComp.tsx │ ├── ClapCountComp.tsx │ └── NewStory.tsx ├── favorite.ts └── actions │ ├── user.ts │ ├── topics.ts │ ├── comments.ts │ ├── claps.ts │ └── story.ts ├── postcss.config.mjs ├── drizzle.config.ts ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeScrapper1/medium-clone/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeScrapper1/medium-clone/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: String; 3 | name?: String; 4 | email: String; 5 | emailVerified?: any; 6 | image: String; 7 | }; 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth"; 2 | import NextAuth from "next-auth"; 3 | 4 | const handler = NextAuth(authOptions); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/db/drizzle.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import { drizzle } from "drizzle-orm/neon-http"; 3 | 4 | import * as schema from "./schema"; 5 | 6 | const sql = neon(process.env.DATABASE_URL!); 7 | 8 | const db = drizzle(sql, { schema }); 9 | 10 | export default db; 11 | -------------------------------------------------------------------------------- /src/lib/style.css: -------------------------------------------------------------------------------- 1 | h1:empty:not(:focus)::before { 2 | content: attr(data-h1-placeholder); 3 | opacity: 30%; 4 | } 5 | 6 | p:empty:not(:focus)::before { 7 | content: attr(data-p-placeholder); 8 | opacity: 30%; 9 | } 10 | 11 | .medium-editor-placeholder:after { 12 | content: "" !important; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SessionProvider } from "next-auth/react"; 3 | import React from "react"; 4 | 5 | const AuthProvider = ({ children }: { children: React.ReactNode }) => { 6 | return {children}; 7 | }; 8 | 9 | export default AuthProvider; 10 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import type { Config } from "drizzle-kit"; 4 | 5 | export default { 6 | schema: "./src/db/schema.ts", 7 | out: "./src/db/drizzle.ts", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL!, 10 | }, 11 | dialect: "postgresql", 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { protocol: "http", hostname: "res.cloudinary.com" }, 6 | { protocol: "https", hostname: "lh3.googleusercontent.com" }, 7 | ], 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /src/app/addtopics/page.tsx: -------------------------------------------------------------------------------- 1 | import { SelectedTopics } from "@/actions/topics"; 2 | import AddTags from "@/components/AddTags"; 3 | import React from "react"; 4 | 5 | const AddTopics = async () => { 6 | const userTags = await SelectedTopics(); 7 | return ; 8 | }; 9 | 10 | export default AddTopics; 11 | -------------------------------------------------------------------------------- /src/components/GetStories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StoryDetail from "./StoryDetail"; 3 | 4 | const GetStories = ({ stories }: any) => { 5 | return ( 6 |
7 | {stories?.map((story: any, index: number) => ( 8 | 9 | ))} 10 |
11 | ); 12 | }; 13 | 14 | export default GetStories; 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/Separator.tsx: -------------------------------------------------------------------------------- 1 | import { MoreHorizontal } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const Separator = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |

11 |
12 | ); 13 | }; 14 | 15 | export default Separator; 16 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 4 | import db from "@/db/drizzle"; 5 | 6 | export const authOptions: any = { 7 | adapter: DrizzleAdapter(db), 8 | providers: [ 9 | GoogleProvider({ 10 | clientId: process.env.GOOGLE_ID as string, 11 | clientSecret: process.env.GOOGLE_SECRET as string, 12 | }), 13 | ], 14 | }; 15 | 16 | export const getAuthSession = () => getServerSession(authOptions); 17 | -------------------------------------------------------------------------------- /src/favorite.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { and, eq } from "drizzle-orm"; 4 | import { getUser } from "./actions/user"; 5 | import db from "./db/drizzle"; 6 | import { save } from "./db/schema"; 7 | 8 | export const checkFav = async (storyId: string) => { 9 | const user: any = await getUser(); 10 | 11 | let fav; 12 | try { 13 | fav = await db.query.save.findFirst({ 14 | where: and(eq(save.userId, user.id), eq(save.storyId, storyId)), 15 | }); 16 | } catch (error) { 17 | return { status: false }; 18 | } 19 | return !!fav; 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | .env 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SidebarStories from "./SidebarStories"; 3 | import Link from "next/link"; 4 | import Ads from "./Ads"; 5 | 6 | const Sidebar = ({ stories }: any) => { 7 | return ( 8 |
9 |

Staff Picks

10 |
11 | {stories?.map((story: any, index: number) => ( 12 | 13 | ))} 14 |
15 | 16 | See the full list 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default Sidebar; 24 | -------------------------------------------------------------------------------- /src/app/published/[storyId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getStoryById } from "@/actions/story"; 2 | import { getUserById } from "@/actions/user"; 3 | import StoryRender from "@/components/StoryRender"; 4 | import React from "react"; 5 | 6 | const Published = async ({ params }: { params: { storyId: string } }) => { 7 | const publishedStory: any = await getStoryById(params?.storyId, true); 8 | 9 | if (!publishedStory) { 10 | return
Not story found
; 11 | } 12 | 13 | const user: any = await getUserById(publishedStory?.userId); 14 | 15 | return ( 16 | 21 | ); 22 | }; 23 | 24 | export default Published; 25 | -------------------------------------------------------------------------------- /src/lib/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const uploadImage = async (formData: FormData) => { 4 | const file = formData.get("file"); 5 | formData.append( 6 | "upload_preset", 7 | `${process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET}` 8 | ); 9 | 10 | if (!file) { 11 | return { error: "upload fialed" }; 12 | } 13 | 14 | const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_NAME}/image/upload`; 15 | 16 | try { 17 | const res = await axios.post(cloudinaryUrl, formData, { 18 | headers: { 19 | "Content-Type": "multipart/form-data", 20 | }, 21 | }); 22 | return res.data.url; 23 | } catch (error) { 24 | return { error: "error in uploading image" }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/p/[storyId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getStoryById } from "@/actions/story"; 2 | import { getUser } from "@/actions/user"; 3 | import NavbarStory from "@/components/NavbarStory"; 4 | import NewStory from "@/components/NewStory"; 5 | import { User } from "@/interfaces"; 6 | import React from "react"; 7 | 8 | const StoryId = async ({ params }: { params: { storyId: string } }) => { 9 | const story: any = await getStoryById(params?.storyId, false); 10 | const user: any = await getUser(); 11 | console.log(user, "story"); 12 | return ( 13 |
14 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default StoryId; 24 | -------------------------------------------------------------------------------- /src/components/SidebarStories.tsx: -------------------------------------------------------------------------------- 1 | import { contentFormat } from "@/lib/data"; 2 | import React from "react"; 3 | 4 | const SidebarStories = async ({ story }: any) => { 5 | const formattedContent: any = await contentFormat(story?.content, 20); 6 | return ( 7 |
8 |
9 | 14 |
15 |

16 | {formattedContent?.h1ElementWithoutTag} 17 |

18 | {formattedContent?.firstWords} 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default SidebarStories; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import AuthProvider from "@/lib/AuthProvider"; 5 | import Header from "@/components/Header"; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 | 25 |
26 | {children} 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/Ads.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const Ads = () => { 5 | return ( 6 |
7 |

Writing on Medium

8 |
    9 |
  • 10 | 11 | New Writer FAQ 12 | 13 |
  • 14 |
  • 15 | 16 | Expert writing advice 17 | 18 |
  • 19 |
  • 20 | 21 | Grow your readership 22 | 23 |
  • 24 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Ads; 33 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/UserBadget.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | 4 | type Props = { 5 | user: any; 6 | createdAt: Date; 7 | }; 8 | const UserBadget = ({ user, createdAt }: Props) => { 9 | const calculateDaysAgo = () => { 10 | const currentDate = new Date(); 11 | const createdDate = new Date(createdAt); 12 | const timeDifference: number = 13 | currentDate.getTime() - createdDate.getTime(); 14 | const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); 15 | return daysAgo; 16 | }; 17 | return ( 18 |
19 |
20 | user image 28 |
29 |

{user?.name}

30 |

{calculateDaysAgo()}

31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default UserBadget; 38 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { getLimitedStories, getStories } from "@/actions/story"; 2 | import { getUniqueTopics, SelectedTopics } from "@/actions/topics"; 3 | import GetStories from "@/components/GetStories"; 4 | import Sidebar from "@/components/Sidebar"; 5 | import Topics from "@/components/Topics"; 6 | 7 | export default async function Home({ 8 | searchParams, 9 | }: { 10 | searchParams: { tag: string }; 11 | }) { 12 | const allTopics = await getUniqueTopics(); 13 | const getSelectedTopics = await SelectedTopics(); 14 | const stories = await getStories(searchParams?.tag); 15 | const limitedStories = await getLimitedStories(searchParams?.tag); 16 | console.log(limitedStories, "stories"); 17 | return ( 18 |
19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/actions/user.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import db from "@/db/drizzle"; 4 | import { user } from "@/db/schema"; 5 | import { getAuthSession } from "@/lib/auth"; 6 | import { eq } from "drizzle-orm"; 7 | 8 | export const getUser = async () => { 9 | const session: any = await getAuthSession(); 10 | if (!session) { 11 | return { 12 | error: "user not found", 13 | }; 14 | } 15 | let userDetails; 16 | try { 17 | userDetails = await db.query.user.findFirst({ 18 | where: eq(user?.email, session.user.email), 19 | }); 20 | if (!userDetails) { 21 | return { error: "user not found" }; 22 | } 23 | } catch (error) { 24 | return { error: "user not found" }; 25 | } 26 | return userDetails; 27 | }; 28 | 29 | export const getUserById = async (userId: string) => { 30 | let userDetails; 31 | try { 32 | userDetails = await db.query.user.findFirst({ 33 | where: eq(user?.id, userId), 34 | }); 35 | if (!userDetails) { 36 | return { error: "user not found" }; 37 | } 38 | } catch (error) { 39 | return { error: "user not found" }; 40 | } 41 | return userDetails; 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/Favorite.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { addToFav } from "@/actions/story"; 3 | import React from "react"; 4 | 5 | type Props = { 6 | storyId: string; 7 | favStatus: boolean; 8 | }; 9 | const Favorite = ({ storyId, favStatus }: Props) => { 10 | const FavStory = async () => { 11 | await addToFav(storyId); 12 | }; 13 | return ( 14 | 31 | ); 32 | }; 33 | 34 | export default Favorite; 35 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ImageComp.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { uploadImage } from "@/lib/cloudinary"; 3 | import React, { useEffect, useState } from "react"; 4 | 5 | type Props = { 6 | imageUrl: string; 7 | file: File; 8 | handleSave: () => void; 9 | }; 10 | const ImageComp = ({ imageUrl, file, handleSave }: Props) => { 11 | const [currentImageUrl, setCurrentImageUrl] = useState(imageUrl); 12 | console.log(currentImageUrl, "currentImageUrl"); 13 | 14 | const updateImage = async () => { 15 | try { 16 | const formData = new FormData(); 17 | formData.append("file", file); 18 | await uploadImage(formData).then((imgUrl: any) => { 19 | console.log(imgUrl, "imgUrl"); 20 | setCurrentImageUrl(imgUrl); 21 | }); 22 | } catch (error) {} 23 | }; 24 | useEffect(() => { 25 | updateImage().then(() => { 26 | handleSave(); 27 | }); 28 | }, [imageUrl]); 29 | return ( 30 |
31 |
32 | Image 37 |
38 |

39 |
40 |
41 |

42 |
43 | ); 44 | }; 45 | 46 | export default ImageComp; 47 | -------------------------------------------------------------------------------- /src/lib/data.ts: -------------------------------------------------------------------------------- 1 | export const contentFormat = async (content: string, words: number) => { 2 | const stripeHtmlTags = (htmlString: string) => { 3 | return htmlString?.replace(/<[^>]*>/g, ""); 4 | }; 5 | 6 | const contentWithoutH1 = content?.replace(/]*>[\s\S]*?<\/h1>/g, ""); 7 | 8 | const finalContent = contentWithoutH1?.replace( 9 | /]*>[\s\S]*?<\/h1>|]*>[\s\S]*?<\/select>|]*>[\s\S]*?<\/textarea>/gi, 10 | "" 11 | ); 12 | 13 | const textwithoutHtml = stripeHtmlTags(contentWithoutH1); 14 | const firstWords = textwithoutHtml?.split(/\s+/)?.splice(0, words)?.join(" "); 15 | 16 | const h1Match = content?.match(/]*>([\s\S]*?)<\/h1>/); 17 | const h1ElementWithoutTag = h1Match ? stripeHtmlTags(h1Match[1]) : ""; 18 | 19 | const imageMatch = content?.match(/]*src=["']([^"']*)["'][^>]*>/); 20 | const imgSrc = imageMatch ? imageMatch[1] : ""; 21 | 22 | return { 23 | h1ElementWithoutTag, 24 | textwithoutHtml, 25 | finalContent, 26 | firstWords, 27 | imgSrc, 28 | }; 29 | }; 30 | 31 | export const topics = [ 32 | { value: "Javascript", label: "Javascript" }, 33 | { value: "Python", label: "Python" }, 34 | { value: "Programming", label: "Programming" }, 35 | { value: "php", label: "php" }, 36 | { value: "java", label: "java" }, 37 | { value: "angular", label: "angular" }, 38 | ]; 39 | -------------------------------------------------------------------------------- /src/components/Share.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | import React from "react"; 4 | import { toast } from "./ui/use-toast"; 5 | 6 | const Share = () => { 7 | const pathname = usePathname(); 8 | return ( 9 |
10 | 28 |
29 | ); 30 | }; 31 | 32 | export default Share; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth/drizzle-adapter": "^1.2.0", 13 | "@neondatabase/serverless": "^0.9.3", 14 | "@radix-ui/react-avatar": "^1.0.4", 15 | "@radix-ui/react-toast": "^1.1.5", 16 | "axios": "^1.7.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "crypto": "^1.0.1", 20 | "dotenv": "^16.4.5", 21 | "drizzle-orm": "^0.31.0", 22 | "highlight.js": "^11.9.0", 23 | "lucide-react": "^0.383.0", 24 | "medium-editor": "^5.23.3", 25 | "next": "^15.0.0-rc.0", 26 | "next-auth": "^4.24.7", 27 | "react": "^19.0.0-rc-6d3110b4d9-20240531", 28 | "react-dom": "^19.0.0-rc-6d3110b4d9-20240531", 29 | "react-select": "^5.8.0", 30 | "tailwind-merge": "^2.3.0", 31 | "tailwindcss-animate": "^1.0.7" 32 | }, 33 | "devDependencies": { 34 | "@types/medium-editor": "^5.0.8", 35 | "@types/node": "^20", 36 | "@types/react": "^18", 37 | "@types/react-dom": "^18", 38 | "drizzle-kit": "^0.22.1", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.2.3", 41 | "pg": "^8.12.0", 42 | "postcss": "^8", 43 | "tailwindcss": "^3.4.1", 44 | "typescript": "^5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signIn, useSession } from "next-auth/react"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/navigation"; 5 | import React from "react"; 6 | 7 | const Login = () => { 8 | const router = useRouter(); 9 | const { status } = useSession(); 10 | console.log(status); 11 | 12 | if (status === "authenticated") { 13 | router.push("/"); 14 | } 15 | return ( 16 |
17 |
18 | 19 |

Login into continue

20 |
signIn("google")} 23 | > 24 | 29 | Sign in with Google 30 |
31 | 35 | Go to Home page 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default Login; 43 | -------------------------------------------------------------------------------- /src/components/ReplyComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UserBadget from "./UserBadget"; 3 | import ClapCountComp from "./ClapCountComp"; 4 | 5 | type Props = { 6 | storyId: string; 7 | comment: any; 8 | }; 9 | const ReplyComponent = ({ storyId, comment }: Props) => { 10 | return ( 11 |
12 | {comment?.replies?.map((reply: any, index: number) => { 13 | const clapCounts = reply?.clap?.map((clap: any) => clap?.clapCount); 14 | const totalClaps = clapCounts?.reduce( 15 | (acc: any, curr: number) => acc + curr, 16 | 0 17 | ); 18 | return ( 19 |
23 | 24 |

25 | {reply?.content} 26 |

27 |
28 |
29 | 36 |
37 |
38 |
39 | ); 40 | })} 41 |
42 | ); 43 | }; 44 | 45 | export default ReplyComponent; 46 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/AddTags.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { addRemoveTags } from "@/actions/topics"; 3 | import { topics } from "@/lib/data"; 4 | import React, { useState } from "react"; 5 | import Select from "react-select"; 6 | import { toast } from "./ui/use-toast"; 7 | 8 | const AddTags = ({ userTags }: any) => { 9 | const [selectedTopics, setSelectedTopics] = useState([]); 10 | 11 | const AdduserTags = async () => { 12 | const res = await addRemoveTags(selectedTopics); 13 | if (res?.error) { 14 | toast({ title: res.error }); 15 | } 16 | }; 17 | return ( 18 |
19 |
20 |
21 |