├── .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 |
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 |
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 |

37 |
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>|