├── middlewares ├── index.ts └── withUser │ ├── index.ts │ ├── WithUser.types.ts │ └── withUser.ts ├── components ├── buttons │ ├── index.ts │ └── PrimaryButton │ │ ├── index.ts │ │ ├── PrimaryButton.types.ts │ │ └── PrimaryButton.tsx ├── layout │ ├── Footer │ │ ├── index.ts │ │ └── Footer.tsx │ ├── Container │ │ ├── index.ts │ │ ├── Container.types.ts │ │ └── Container.tsx │ ├── popularCheatsheets │ │ ├── PopularCheatsheets.types.ts │ │ ├── index.ts │ │ ├── SmallCard.types.ts │ │ ├── PopularCheatsheets.tsx │ │ └── SmallCard.tsx │ ├── index.ts │ └── Navbar.tsx ├── pages │ ├── Home │ │ ├── index.ts │ │ └── Home.types.ts │ ├── cheatsheet │ │ ├── index.ts │ │ ├── Card.types.ts │ │ └── Card.tsx │ ├── dashboard │ │ ├── index.ts │ │ ├── CheatsheetItem.types.ts │ │ └── CheatsheetItem.tsx │ └── index.ts ├── inputs │ ├── index.ts │ ├── TextArea │ │ ├── index.ts │ │ ├── TextArea.types.ts │ │ └── TextArea.tsx │ └── PrimaryInput │ │ ├── index.ts │ │ ├── PrimaryInput.types.ts │ │ └── PrimaryInput.tsx ├── typography │ ├── H1 │ │ ├── index.ts │ │ ├── H1.types.ts │ │ └── H1.tsx │ ├── H2 │ │ ├── index.ts │ │ ├── H2.types.ts │ │ └── H2.tsx │ ├── H3 │ │ ├── index.ts │ │ ├── H3.types.ts │ │ └── H3.tsx │ ├── Label │ │ ├── index.ts │ │ ├── Label.types.ts │ │ └── Label.tsx │ ├── Detail │ │ ├── index.ts │ │ ├── Detail.types.ts │ │ └── Detail.tsx │ └── index.ts ├── index.ts └── Loader.tsx ├── db ├── index.ts ├── models │ ├── index.ts │ ├── View.ts │ ├── Favourite.ts │ ├── Like.ts │ ├── Cheatsheet.ts │ └── User.ts └── db.ts ├── .eslintrc.json ├── public ├── favicon.ico └── assets │ └── logo.svg ├── lib ├── index.ts ├── client.ts ├── genToken.ts ├── twitter.ts └── store.ts ├── postcss.config.js ├── .prettierrc ├── styles └── globals.css ├── .env.example ├── next-env.d.ts ├── next.config.js ├── types.d.ts ├── .gitignore ├── pages ├── api │ ├── cheatsheets │ │ ├── [id] │ │ │ ├── likes.ts │ │ │ ├── isLiked.ts │ │ │ ├── isFaved.ts │ │ │ ├── view.ts │ │ │ ├── like.ts │ │ │ ├── favorite.ts │ │ │ └── index.ts │ │ ├── popular.ts │ │ ├── my.ts │ │ ├── index.ts │ │ └── favorites.ts │ └── auth │ │ ├── twitter │ │ ├── login.ts │ │ └── authorize.ts │ │ └── me.ts ├── _document.tsx ├── _app.tsx ├── index.tsx ├── cheatsheets │ ├── edit │ │ └── [id].tsx │ └── [id].tsx ├── create.tsx └── dashboard.tsx ├── tailwind.config.js ├── tsconfig.json ├── package.json ├── README.md └── CONTRIBUTING.md /middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./withUser" 2 | -------------------------------------------------------------------------------- /components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PrimaryButton" 2 | -------------------------------------------------------------------------------- /components/layout/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Footer" 2 | -------------------------------------------------------------------------------- /components/pages/Home/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Home.types" 2 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db' 2 | export * from './models' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /components/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PrimaryInput" 2 | export * from "./TextArea" 3 | -------------------------------------------------------------------------------- /components/typography/H1/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./H1" 2 | export * from "./H1.types" 3 | -------------------------------------------------------------------------------- /components/typography/H2/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./H2" 2 | export * from "./H2.types" 3 | -------------------------------------------------------------------------------- /components/typography/H3/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./H3" 2 | export * from "./H3.types" 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcodesdev/cheatly/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/pages/cheatsheet/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Card" 2 | export * from "./Card.types" 3 | -------------------------------------------------------------------------------- /components/typography/Label/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Label" 2 | export * from "./Label.types" 3 | -------------------------------------------------------------------------------- /middlewares/withUser/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./withUser" 2 | export * from "./WithUser.types" 3 | -------------------------------------------------------------------------------- /components/inputs/TextArea/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./TextArea" 2 | export * from "./TextArea.types" 3 | -------------------------------------------------------------------------------- /components/typography/Detail/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Detail" 2 | export * from "./Detail.types" 3 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client" 2 | export * from "./genToken" 3 | export * from "./store" 4 | -------------------------------------------------------------------------------- /components/layout/Container/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Container" 2 | export * from "./Container.types" 3 | -------------------------------------------------------------------------------- /components/layout/popularCheatsheets/PopularCheatsheets.types.ts: -------------------------------------------------------------------------------- 1 | export interface IPopularCheatsheetsProps {} -------------------------------------------------------------------------------- /components/inputs/PrimaryInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PrimaryInput" 2 | export * from "./PrimaryInput.types" 3 | -------------------------------------------------------------------------------- /components/pages/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CheatsheetItem" 2 | export * from "./CheatsheetItem.types" 3 | -------------------------------------------------------------------------------- /components/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Home" 2 | export * from "./cheatsheet" 3 | export * from "./dashboard" 4 | -------------------------------------------------------------------------------- /components/buttons/PrimaryButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PrimaryButton" 2 | export * from "./PrimaryButton.types" 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /components/pages/Home/Home.types.ts: -------------------------------------------------------------------------------- 1 | export interface IHomeProps { 2 | cheatsheetCount: number 3 | userCount: number 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /middlewares/withUser/WithUser.types.ts: -------------------------------------------------------------------------------- 1 | import { ApiHandler } from "@types" 2 | 3 | export type WithUser = (handler: ApiHandler) => ApiHandler 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | font-family: "Poppins", sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Container" 2 | export * from "./Footer" 3 | export * from "./popularCheatsheets" 4 | export * from "./Navbar" 5 | -------------------------------------------------------------------------------- /components/layout/Container/Container.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export interface IContainerProps { 4 | children: ReactNode 5 | } 6 | -------------------------------------------------------------------------------- /components/typography/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./H1" 2 | export * from "./H2" 3 | export * from "./H3" 4 | export * from "./Detail" 5 | export * from "./Label" 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URI= 2 | TWITTER_API_KEY= 3 | TWITTER_API_SECRET= 4 | TWITTER_ACCESS_TOKEN= 5 | TWITTER_ACCESS_SECRET= 6 | SERVER_URL= 7 | CLIENT_URL= 8 | JWT_SECRET= -------------------------------------------------------------------------------- /db/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./User" 2 | export * from "./Cheatsheet" 3 | export * from "./Favourite" 4 | export * from "./Like" 5 | export * from "./View" 6 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buttons" 2 | export * from "./layout" 3 | export * from "./pages" 4 | export * from "./typography" 5 | export * from "./Loader" 6 | export * from "./inputs" 7 | -------------------------------------------------------------------------------- /components/layout/popularCheatsheets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PopularCheatsheets" 2 | export * from "./SmallCard" 3 | export * from "./PopularCheatsheets.types" 4 | export * from "./SmallCard.types" 5 | -------------------------------------------------------------------------------- /components/typography/Label/Label.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface ILabelProps 4 | extends DetailedHTMLProps, HTMLDivElement> {} 5 | -------------------------------------------------------------------------------- /lib/client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import Cookies from "js-cookie" 3 | 4 | export const client = axios.create({ 5 | headers: { 6 | Authorization: `Bearer ${Cookies.get("token")}`, 7 | }, 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /lib/genToken.ts: -------------------------------------------------------------------------------- 1 | import JWT from "jsonwebtoken" 2 | 3 | export const genToken = (id: string) => { 4 | const token = JWT.sign({ id }, process.env.JWT_SECRET as string, { 5 | expiresIn: "7d", 6 | }) 7 | return token 8 | } 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ["i.ibb.co", "pbs.twimg.com"], 6 | }, 7 | } 8 | 9 | module.exports = nextConfig 10 | -------------------------------------------------------------------------------- /components/typography/H1/H1.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface IH1Props 4 | extends DetailedHTMLProps< 5 | HTMLAttributes, 6 | HTMLHeadingElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/typography/H2/H2.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface IH2Props 4 | extends DetailedHTMLProps< 5 | HTMLAttributes, 6 | HTMLHeadingElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/typography/H3/H3.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface IH3Props 4 | extends DetailedHTMLProps< 5 | HTMLAttributes, 6 | HTMLHeadingElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/layout/popularCheatsheets/SmallCard.types.ts: -------------------------------------------------------------------------------- 1 | import { UserType } from "@db" 2 | import { CheatsheetWLikesAndViews } from "@lib" 3 | 4 | export interface ISmallCardProps { 5 | cheatsheet: CheatsheetWLikesAndViews & { author: Partial } 6 | } 7 | -------------------------------------------------------------------------------- /components/pages/dashboard/CheatsheetItem.types.ts: -------------------------------------------------------------------------------- 1 | import { CheatsheetType } from "@db" 2 | 3 | export interface ICheatsheetItemProps { 4 | cheatsheet: CheatsheetType & { likes: number; views: number } 5 | deleteHandler: (id: string) => Promise 6 | } 7 | -------------------------------------------------------------------------------- /components/typography/Detail/Detail.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface IDetailProps 4 | extends DetailedHTMLProps< 5 | HTMLAttributes, 6 | HTMLParagraphElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/inputs/PrimaryInput/PrimaryInput.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, InputHTMLAttributes } from "react" 2 | 3 | export interface IInputProps 4 | extends DetailedHTMLProps< 5 | InputHTMLAttributes, 6 | HTMLInputElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/pages/cheatsheet/Card.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from "react" 2 | 3 | export interface ICardProps 4 | extends DetailedHTMLProps, HTMLDivElement> { 5 | text: string 6 | cardNumber: string 7 | } 8 | -------------------------------------------------------------------------------- /components/inputs/TextArea/TextArea.types.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, TextareaHTMLAttributes } from "react" 2 | 3 | export interface ITextAreaProps 4 | extends DetailedHTMLProps< 5 | TextareaHTMLAttributes, 6 | HTMLTextAreaElement 7 | > {} 8 | -------------------------------------------------------------------------------- /components/layout/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IContainerProps } from './Container.types' 3 | 4 | export const Container: FC = ({ children }) => { 5 | return
{children}
6 | } 7 | -------------------------------------------------------------------------------- /components/buttons/PrimaryButton/PrimaryButton.types.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, DetailedHTMLProps } from "react" 2 | 3 | export interface IPrimaryButtonProps 4 | extends DetailedHTMLProps< 5 | ButtonHTMLAttributes, 6 | HTMLButtonElement 7 | > { 8 | variant?: 1 | 2 9 | } 10 | -------------------------------------------------------------------------------- /components/typography/Detail/Detail.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IDetailProps } from '.' 3 | 4 | export const Detail: FC = ({ className, children, ...rest }) => { 5 | return ( 6 |

7 | {children} 8 |

9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next" 2 | import type { TwitterApi } from "twitter-api-v2" 3 | import type { UserType } from "@db" 4 | 5 | export type Request = NextApiRequest & { user: UserType; client: TwitterApi } 6 | 7 | export type ApiHandler = (req: Request, res: NextApiResponse) => Promise 8 | -------------------------------------------------------------------------------- /db/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | 3 | export const connectDb = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGO_URI as string) 6 | console.log("MongoDB connected") 7 | } catch (error) { 8 | if (error instanceof Error) console.log(error.message) 9 | } 10 | } 11 | 12 | connectDb() 13 | -------------------------------------------------------------------------------- /components/typography/H2/H2.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IH2Props } from '.' 3 | 4 | export const H2: FC = ({ children, className, ...rest }) => { 5 | return ( 6 |

9 | {children} 10 |

11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/typography/H1/H1.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IH1Props } from '.' 3 | 4 | export const H1: FC = ({ children, className, ...rest }) => { 5 | return ( 6 |

10 | {children} 11 |

12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /components/typography/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { ILabelProps } from '.' 3 | 4 | export const Label: FC = ({ children, className, ...rest }) => { 5 | return ( 6 |
10 | {children} 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /components/typography/H3/H3.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IH3Props } from './H3.types' 3 | 4 | export const H3: FC = ({ children, className, ...rest }) => { 5 | return ( 6 |

10 | {children} 11 |

12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /components/inputs/PrimaryInput/PrimaryInput.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IInputProps } from '.' 3 | 4 | export const PrimaryInput: FC = ({ className, ...rest }) => { 5 | return ( 6 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/inputs/TextArea/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { ITextAreaProps } from '.' 3 | 4 | export const TextArea: FC = ({ 5 | rows = 3, 6 | className, 7 | ...rest 8 | }) => { 9 | return ( 10 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | .env -------------------------------------------------------------------------------- /lib/twitter.ts: -------------------------------------------------------------------------------- 1 | import TwitterApi from "twitter-api-v2" 2 | 3 | if ( 4 | !process.env.TWITTER_API_KEY || 5 | !process.env.TWITTER_API_SECRET || 6 | !process.env.TWITTER_ACCESS_TOKEN || 7 | !process.env.TWITTER_ACCESS_SECRET 8 | ) 9 | throw Error("Missing Twitter API credentials") 10 | 11 | const Twitter = new TwitterApi({ 12 | appKey: process.env.TWITTER_API_KEY, 13 | appSecret: process.env.TWITTER_API_SECRET, 14 | accessToken: process.env.TWITTER_ACCESS_TOKEN, 15 | accessSecret: process.env.TWITTER_ACCESS_SECRET, 16 | }) 17 | 18 | export default Twitter 19 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { TailSpin } from 'react-loader-spinner' 2 | import { useStore } from '../lib/store' 3 | 4 | export const Loader = () => { 5 | const loading = useStore((state) => state.loading) 6 | 7 | if (!loading) return <> 8 | 9 | return ( 10 |
14 |
15 | 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/likes.ts: -------------------------------------------------------------------------------- 1 | import { ApiHandler } from "@types" 2 | import Like from "@db/models/Like" 3 | 4 | const handler: ApiHandler = async (req, res) => { 5 | if (req.method !== "GET") { 6 | res.status(405).end() 7 | return 8 | } 9 | 10 | try { 11 | const { id } = req.query 12 | const likes = await Like.countDocuments({ 13 | cheatsheet_id: id, 14 | }) 15 | 16 | res.json(likes) 17 | } catch (error) { 18 | res.status(400).json({ 19 | message: "Something went wrong", 20 | }) 21 | } 22 | } 23 | 24 | export default handler 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: { 10 | 1: "#FFFFFF", 11 | 2: "#FFFFFF", 12 | 3: "#F7F7F7", 13 | 4: "#FFD124", 14 | pink: { 15 | 1: "#F24A72", 16 | }, 17 | dark: { 18 | 1: "#203239", 19 | }, 20 | }, 21 | }, 22 | }, 23 | }, 24 | plugins: [require("@tailwindcss/line-clamp")], 25 | } 26 | -------------------------------------------------------------------------------- /components/pages/cheatsheet/Card.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { ICardProps } from '.' 3 | 4 | export const Card: FC = ({ 5 | className, 6 | text, 7 | cardNumber, 8 | ...rest 9 | }) => { 10 | return ( 11 |
15 |
{text}
16 | 17 |
18 | {cardNumber} 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/buttons/PrimaryButton/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IPrimaryButtonProps } from '.' 3 | 4 | export const PrimaryButton: FC = ({ 5 | children, 6 | className, 7 | variant = 1, 8 | ...rest 9 | }) => { 10 | return ( 11 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/isLiked.ts: -------------------------------------------------------------------------------- 1 | import { withUser } from "@middlewares" 2 | import { ApiHandler } from "@types" 3 | import Like from "@db/models/Like" 4 | 5 | const handler: ApiHandler = async (req, res) => { 6 | if (req.method !== "GET") { 7 | res.status(405).end() 8 | return 9 | } 10 | 11 | try { 12 | const { id } = req.query 13 | const isLiked = await Like.findOne({ 14 | cheatsheet_id: id, 15 | user_id: req.user._id, 16 | }) 17 | 18 | res.json(!!isLiked) 19 | } catch (error) { 20 | res.status(400).json({ 21 | message: "Something went wrong", 22 | }) 23 | } 24 | } 25 | 26 | export default withUser(handler) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@*": ["*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 13 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /db/models/View.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Model, 3 | model, 4 | models, 5 | Schema, 6 | SchemaTimestampsConfig, 7 | Document, 8 | Types, 9 | } from "mongoose" 10 | import CheatSheet from "@db/models/Cheatsheet" 11 | import "../db" 12 | 13 | interface ViewType extends Document, SchemaTimestampsConfig { 14 | ip: string 15 | cheatsheet_id: string 16 | } 17 | 18 | const viewSchema = new Schema( 19 | { 20 | ip: { 21 | type: String, 22 | }, 23 | cheatsheet_id: { 24 | type: Types.ObjectId, 25 | ref: CheatSheet, 26 | }, 27 | }, 28 | { 29 | timestamps: true, 30 | } 31 | ) 32 | 33 | const View: Model = models.View || model("View", viewSchema) 34 | 35 | export default View 36 | -------------------------------------------------------------------------------- /pages/api/auth/twitter/login.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next" 2 | 3 | import Twitter from "@lib/twitter" 4 | 5 | const GET: NextApiHandler = async (req, res) => { 6 | try { 7 | const callbackUrl = `${process.env.SERVER_URL}/api/auth/twitter/authorize` 8 | const { url } = await Twitter.generateAuthLink(callbackUrl) 9 | 10 | res.redirect(url) 11 | } catch (error) { 12 | if (error instanceof Error) 13 | res.status(400).json({ message: "Internal server error." }) 14 | } 15 | } 16 | 17 | const handler: NextApiHandler = async (req, res) => { 18 | if (req.method !== "GET") { 19 | res.status(405).end() 20 | return 21 | } 22 | 23 | await GET(req, res) 24 | } 25 | 26 | export default handler 27 | -------------------------------------------------------------------------------- /components/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { useUser } from '../../lib/store' 4 | 5 | export const Navbar = () => { 6 | const { user } = useUser() 7 | 8 | return ( 9 |
10 | 11 | 12 | cheatsheet logo 18 | 19 | 20 | 21 | {user && ( 22 | 23 | 24 |

Dashboard

25 |
26 | 27 | )} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/isFaved.ts: -------------------------------------------------------------------------------- 1 | import Favorite from "@db/models/Favourite" 2 | import { withUser } from "@middlewares" 3 | import { ApiHandler } from "@types" 4 | 5 | // to check if the cheat sheet is favourited by the user 6 | const handler: ApiHandler = async (req, res) => { 7 | if (req.method !== "GET") { 8 | res.status(405).end() 9 | return 10 | } 11 | 12 | try { 13 | const { id } = req.query 14 | const { user } = req 15 | 16 | if (!id || typeof id !== "string") throw Error() 17 | 18 | const exists = await Favorite.exists({ 19 | cheatsheet_id: id, 20 | user_id: user._id, 21 | }) 22 | 23 | res.json(exists) 24 | } catch (error) { 25 | res.status(400).json({ 26 | message: "something went wrong", 27 | }) 28 | } 29 | } 30 | 31 | export default withUser(handler) 32 | -------------------------------------------------------------------------------- /db/models/Favourite.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Model, 3 | Schema, 4 | models, 5 | model, 6 | Document, 7 | SchemaTimestampsConfig, 8 | Types, 9 | } from "mongoose" 10 | import CheatSheet from "@db/models/Cheatsheet" 11 | import User from "@db/models/User" 12 | import "../db" 13 | 14 | interface FavoriteType extends Document, SchemaTimestampsConfig { 15 | cheatsheet_id: string 16 | user_id: string 17 | } 18 | 19 | const favoriteSchema = new Schema( 20 | { 21 | cheatsheet_id: { 22 | type: Types.ObjectId, 23 | ref: CheatSheet, 24 | }, 25 | user_id: { 26 | type: Types.ObjectId, 27 | ref: User, 28 | }, 29 | }, 30 | { 31 | timestamps: true, 32 | } 33 | ) 34 | 35 | favoriteSchema.index({ cheatsheet_id: 1, user_id: 1 }, { unique: true }) 36 | 37 | const Favorite: Model = 38 | models.Favorite || model("Favorite", favoriteSchema) 39 | 40 | export default Favorite 41 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/view.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import requestIp from "request-ip" 3 | 4 | import { ApiHandler } from "@types" 5 | import View from "@db/models/View" 6 | 7 | const handler: ApiHandler = async (req, res) => { 8 | try { 9 | const { id } = req.query 10 | if (!id) throw Error() 11 | if (typeof id !== "string") throw Error() 12 | 13 | const ip = requestIp.getClientIp(req) 14 | 15 | const userViewed = await View.findOne({ 16 | ip, 17 | cheatsheet_id: id, 18 | createdAt: { 19 | $gte: moment().subtract(15, "minutes").toDate(), 20 | }, 21 | }) 22 | 23 | if (!userViewed) { 24 | await View.create({ ip, cheatsheet_id: id }) 25 | res.status(200).end() 26 | return 27 | } 28 | 29 | res.status(200).end() 30 | } catch (error) { 31 | res.status(400).json({ 32 | message: "Something went wrong", 33 | }) 34 | } 35 | } 36 | 37 | export default handler 38 | -------------------------------------------------------------------------------- /db/models/Like.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Schema, 3 | Model, 4 | models, 5 | model, 6 | Document, 7 | Types, 8 | SchemaTimestampsConfig, 9 | } from "mongoose" 10 | import { UserType, CheatsheetType } from "@db" 11 | import User from "@db/models/User" 12 | import CheatSheet from "@db/models/Cheatsheet" 13 | import "../db" 14 | 15 | interface LikeType extends Document, SchemaTimestampsConfig { 16 | user_id: string | UserType 17 | cheatsheet_id: string | CheatsheetType 18 | } 19 | 20 | const likeSchema = new Schema( 21 | { 22 | user_id: { 23 | type: Types.ObjectId, 24 | ref: User, 25 | }, 26 | cheatsheet_id: { 27 | type: Types.ObjectId, 28 | ref: CheatSheet, 29 | }, 30 | }, 31 | 32 | { 33 | timestamps: true, 34 | } 35 | ) 36 | 37 | likeSchema.index({ user_id: 1, cheatsheet_id: 1 }, { unique: true }) 38 | 39 | const Like: Model = models.Like || model("Like", likeSchema) 40 | 41 | export default Like 42 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/like.ts: -------------------------------------------------------------------------------- 1 | import { withUser } from "@middlewares" 2 | import { ApiHandler } from "@types" 3 | import Like from "@db/models/Like" 4 | 5 | const handler: ApiHandler = async (req, res) => { 6 | if (req.method !== "GET") { 7 | res.status(405).end() 8 | return 9 | } 10 | 11 | try { 12 | const { id } = req.query 13 | const { user } = req 14 | 15 | const like = await Like.findOne({ 16 | cheatsheet_id: id, 17 | user_id: user._id, 18 | }) 19 | 20 | if (!like) { 21 | await Like.create({ 22 | cheatsheet_id: id, 23 | user_id: user._id, 24 | }) 25 | } 26 | if (like) { 27 | await Like.deleteOne({ 28 | cheatsheet_id: id, 29 | user_id: user._id, 30 | }) 31 | } 32 | 33 | res.status(200).end() 34 | } catch (error) { 35 | res.status(400).json({ 36 | message: "Something went wrong", 37 | }) 38 | } 39 | } 40 | 41 | export default withUser(handler) 42 | -------------------------------------------------------------------------------- /components/layout/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | import { AiFillGithub } from 'react-icons/ai' 4 | 5 | export const Footer = () => { 6 | return ( 7 |
8 |
9 | This project is made by 10 | 11 | 12 | @Dawson 13 | 14 | 15 |
16 | 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/favorite.ts: -------------------------------------------------------------------------------- 1 | import Favorite from "@db/models/Favourite" 2 | import { withUser } from "@middlewares" 3 | import { ApiHandler } from "@types" 4 | 5 | const handler: ApiHandler = async (req, res) => { 6 | if (req.method !== "GET") { 7 | res.status(405).end() 8 | return 9 | } 10 | 11 | try { 12 | const { id } = req.query 13 | const { user } = req 14 | 15 | if (!id || typeof id !== "string") throw Error() 16 | 17 | const exists = await Favorite.exists({ 18 | cheatsheet_id: id, 19 | user_id: user._id, 20 | }) 21 | 22 | if (exists) { 23 | await Favorite.deleteOne({ 24 | cheatsheet_id: id, 25 | user_id: user._id, 26 | }) 27 | } else { 28 | await Favorite.create({ 29 | cheatsheet_id: id, 30 | user_id: user._id, 31 | }) 32 | } 33 | 34 | res.status(200).end() 35 | } catch (error) { 36 | res.status(400).json({ 37 | message: "something went wrong", 38 | }) 39 | } 40 | } 41 | 42 | export default withUser(handler) 43 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/popular.ts: -------------------------------------------------------------------------------- 1 | import { ApiHandler } from "@types" 2 | import CheatSheet from "@db/models/Cheatsheet" 3 | import Like from "@db/models/Like" 4 | import User from "@db/models/User" 5 | import View from "@db/models/View" 6 | 7 | const handler: ApiHandler = async (req, res) => { 8 | try { 9 | const popularCheatsheets = await CheatSheet.find({ 10 | verified: true, 11 | }) 12 | .limit(10) 13 | .sort({ createdAt: "desc" }) 14 | .lean() 15 | 16 | const promises = popularCheatsheets.map(async (cheatsheet) => ({ 17 | ...cheatsheet, 18 | author: await User.findOne({ _id: cheatsheet.user_id }).select( 19 | "name username profile_picture" 20 | ), 21 | likes: await Like.countDocuments({ cheatsheet_id: cheatsheet._id }), 22 | views: await View.countDocuments({ cheatsheet_id: cheatsheet._id }), 23 | })) 24 | 25 | const data = await Promise.all(promises) 26 | 27 | res.json(data) 28 | } catch (error) { 29 | res.status(400).json({ 30 | message: "something went wrong", 31 | }) 32 | } 33 | } 34 | 35 | export default handler 36 | -------------------------------------------------------------------------------- /components/layout/popularCheatsheets/PopularCheatsheets.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { client, usePopularCheatsheets } from '@lib' 3 | import { SmallCard } from '.' 4 | 5 | export const PopularCheatsheets = () => { 6 | const { setPopularCheatsheets, popularCheatsheets } = usePopularCheatsheets() 7 | 8 | const getPopularCheatsheets = () => { 9 | client 10 | .get('/api/cheatsheets/popular') 11 | .then((r) => { 12 | setPopularCheatsheets(r.data) 13 | }) 14 | .catch((e) => { 15 | console.log(e.message) 16 | }) 17 | } 18 | 19 | useEffect(() => { 20 | getPopularCheatsheets() 21 | }, []) // eslint-disable-line 22 | 23 | return !!popularCheatsheets.length ? ( 24 | <> 25 |

26 | Popular cheatsheets 27 |

28 | 29 |
30 | {popularCheatsheets?.map((ch) => ( 31 | 32 | ))} 33 |
34 | 35 | ) : ( 36 | <> 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/my.ts: -------------------------------------------------------------------------------- 1 | import CheatSheet from "@db/models/Cheatsheet" 2 | import Like from "@db/models/Like" 3 | import View from "@db/models/View" 4 | import { withUser } from "@middlewares" 5 | import { ApiHandler } from "@types" 6 | 7 | const handler: ApiHandler = async (req, res) => { 8 | if (req.method !== "GET") { 9 | res.status(405).end() 10 | return 11 | } 12 | 13 | const page = parseInt(req.query.page?.toString()) || 1 14 | const pageSize = 10 15 | 16 | try { 17 | const myCheatsheets = await CheatSheet.find({ user_id: req.user._id }) 18 | .limit(pageSize) 19 | .skip((page - 1) * pageSize) 20 | .lean() 21 | 22 | const promises = myCheatsheets.map(async (cheatsheet) => ({ 23 | ...cheatsheet, 24 | views: await View.countDocuments({ cheatsheet_id: cheatsheet._id }), 25 | likes: await Like.countDocuments({ cheatsheet_id: cheatsheet._id }), 26 | })) 27 | 28 | const data = await Promise.all(promises) 29 | 30 | res.json(data) 31 | } catch (error) { 32 | if (error instanceof Error) 33 | res.status(400).json({ 34 | message: error.message, 35 | }) 36 | } 37 | } 38 | 39 | export default withUser(handler) 40 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/index.ts: -------------------------------------------------------------------------------- 1 | import CheatSheet from "@db/models/Cheatsheet" 2 | import { withUser } from "@middlewares" 3 | import { ApiHandler } from "@types" 4 | 5 | const POST: ApiHandler = async (req, res) => { 6 | try { 7 | let { name, cards } = req.body 8 | 9 | if (!name || !cards) { 10 | throw new Error("Missing required fields") 11 | } 12 | 13 | cards = cards.filter((card: string) => card) 14 | 15 | if (!cards.length) throw Error("No cards provided") 16 | 17 | cards = cards.map((card: string) => ({ content: card })) 18 | 19 | const cheatsheet = await CheatSheet.create({ 20 | user_id: req.user._id, 21 | name, 22 | cards, 23 | }) 24 | 25 | res.json(cheatsheet) 26 | } catch (error) { 27 | if (error instanceof Error) res.status(400).json({ message: error.message }) 28 | } 29 | } 30 | 31 | const handlers = { 32 | POST, 33 | } 34 | 35 | const handler: ApiHandler = async (req, res) => { 36 | const { method } = req 37 | const handler = handlers[method as keyof typeof handlers] 38 | 39 | if (handler) { 40 | await handler(req, res) 41 | } else { 42 | res.status(404).json({ message: "Method not found" }) 43 | } 44 | } 45 | 46 | export default withUser(handler) 47 | -------------------------------------------------------------------------------- /db/models/Cheatsheet.ts: -------------------------------------------------------------------------------- 1 | import "../db" 2 | import { UserType } from "@db" 3 | import User from "@db/models/User" 4 | import { 5 | Document, 6 | SchemaTimestampsConfig, 7 | Schema, 8 | model, 9 | models, 10 | Model, 11 | Types, 12 | } from "mongoose" 13 | 14 | export interface CheatsheetType extends Document, SchemaTimestampsConfig { 15 | user_id: string | UserType 16 | name: string 17 | cards: Card[] 18 | verified: boolean 19 | } 20 | 21 | interface Card extends Document { 22 | content: string 23 | } 24 | 25 | const cardSchema = new Schema({ 26 | content: { 27 | type: String, 28 | maxlength: 300, 29 | }, 30 | }) 31 | 32 | const cheatsheetSchema = new Schema( 33 | { 34 | name: { 35 | type: String, 36 | maxlength: 320, 37 | }, 38 | cards: { 39 | type: [cardSchema], 40 | maxlength: 100, 41 | }, 42 | verified: { 43 | type: Boolean, 44 | default: false, 45 | }, 46 | user_id: { 47 | type: Types.ObjectId, 48 | ref: User, 49 | }, 50 | }, 51 | { 52 | timestamps: true, 53 | } 54 | ) 55 | 56 | const CheatSheet: Model = 57 | models.CheatSheet || model("CheatSheet", cheatsheetSchema) 58 | 59 | export default CheatSheet 60 | -------------------------------------------------------------------------------- /db/models/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Model, 3 | Document, 4 | SchemaTimestampsConfig, 5 | Schema, 6 | models, 7 | model, 8 | } from "mongoose" 9 | import { CheatsheetType } from "./Cheatsheet" 10 | import "../db" 11 | 12 | export interface UserType extends Document, SchemaTimestampsConfig { 13 | username: string 14 | email: string 15 | twitter_access_token?: string 16 | twitter_access_secret?: string 17 | twitter_user_id: string 18 | profile_picture: string 19 | name: string 20 | cheatsheets: CheatsheetType[] 21 | } 22 | 23 | const UserSchema = new Schema( 24 | { 25 | username: { 26 | type: String, 27 | required: true, 28 | unique: true, 29 | }, 30 | name: { 31 | type: String, 32 | }, 33 | email: { 34 | type: String, 35 | }, 36 | twitter_user_id: { 37 | type: String, 38 | unique: true, 39 | }, 40 | twitter_access_token: { 41 | type: String, 42 | }, 43 | twitter_access_secret: { 44 | type: String, 45 | }, 46 | profile_picture: { 47 | type: String, 48 | }, 49 | }, 50 | { 51 | timestamps: true, 52 | } 53 | ) 54 | 55 | const User: Model = models.User || model("User", UserSchema) 56 | 57 | export default User 58 | -------------------------------------------------------------------------------- /middlewares/withUser/withUser.ts: -------------------------------------------------------------------------------- 1 | import JWT from "jsonwebtoken" 2 | import TwitterApi from "twitter-api-v2" 3 | 4 | import User from "@db/models/User" 5 | import { WithUser } from "." 6 | 7 | export const withUser: WithUser = (handler) => { 8 | return async (req, res) => { 9 | try { 10 | const tokenWithBearer = req.headers.authorization 11 | const token = tokenWithBearer?.split(" ")[1] 12 | 13 | if (!token) throw Error() 14 | 15 | const { id } = JWT.verify(token, process.env.JWT_SECRET as string) as { 16 | id: string 17 | } 18 | 19 | const user = await User.findById(id) 20 | .select("twitter_access_token twitter_access_secret") 21 | .lean() 22 | 23 | if (!user) throw Error() 24 | 25 | const client = new TwitterApi({ 26 | appKey: process.env.TWITTER_API_KEY as string, 27 | appSecret: process.env.TWITTER_API_SECRET as string, 28 | accessToken: user.twitter_access_token, 29 | accessSecret: user.twitter_access_secret, 30 | }) 31 | 32 | req.client = client 33 | req.user = await User.findById(id) 34 | .select("-twitter_access_token -twitter_access_secret") 35 | .lean() 36 | 37 | return await handler(req, res) 38 | } catch (error) { 39 | res.status(401).json({ message: "Unauthenticated" }) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/favorites.ts: -------------------------------------------------------------------------------- 1 | import CheatSheet from "@db/models/Cheatsheet" 2 | import Favorite from "@db/models/Favourite" 3 | import Like from "@db/models/Like" 4 | import View from "@db/models/View" 5 | 6 | import { withUser } from "@middlewares" 7 | import { ApiHandler } from "@types" 8 | 9 | const handler: ApiHandler = async (req, res) => { 10 | if (req.method !== "GET") { 11 | res.status(405).end() 12 | return 13 | } 14 | 15 | try { 16 | const favs = await Favorite.find({ 17 | user_id: req.user._id, 18 | }) 19 | .select("-_id cheatsheet_id") 20 | .lean() 21 | 22 | const cheatsheets = await CheatSheet.find({ 23 | _id: { 24 | $in: favs.map((fav) => fav.cheatsheet_id), 25 | }, 26 | }) 27 | .populate({ path: "user_id", select: "name" }) 28 | .limit(10) 29 | .lean() 30 | 31 | const promises = cheatsheets.map(async (cheatsheet) => ({ 32 | ...cheatsheet, 33 | views: await View.countDocuments({ cheatsheet_id: cheatsheet._id }), 34 | likes: await Like.countDocuments({ cheatsheet_id: cheatsheet._id }), 35 | })) 36 | 37 | const data = await Promise.all(promises) 38 | 39 | res.json(data) 40 | } catch (error) { 41 | res.status(400).json({ 42 | message: "something went wrong", 43 | }) 44 | } 45 | } 46 | 47 | export default withUser(handler) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cheatly", 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 | "test": "ts-node test.ts" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/line-clamp": "^0.3.1", 14 | "axios": "^0.26.1", 15 | "cookie": "^0.5.0", 16 | "dayjs": "^1.11.3", 17 | "fs": "^0.0.1-security", 18 | "js-cookie": "^3.0.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "moment": "^2.29.3", 21 | "mongoose": "^6.3.0", 22 | "next": "12.1.5", 23 | "prettier": "^2.6.2", 24 | "react": "18.0.0", 25 | "react-dom": "18.0.0", 26 | "react-hot-toast": "^2.2.0", 27 | "react-icons": "^4.3.1", 28 | "react-loader-spinner": "^6.0.0-0", 29 | "react-promise-tracker": "^2.1.0", 30 | "react-use": "^17.3.2", 31 | "request-ip": "^2.1.3", 32 | "twitter-api-v2": "^1.12.0", 33 | "zustand": "^4.0.0-rc.0" 34 | }, 35 | "devDependencies": { 36 | "@types/cookie": "^0.5.1", 37 | "@types/fs-extra": "^9.0.13", 38 | "@types/jsonwebtoken": "^8.5.8", 39 | "@types/node": "17.0.25", 40 | "@types/react": "^18.0.5", 41 | "@types/react-dom": "18.0.1", 42 | "@types/request-ip": "^0.0.37", 43 | "autoprefixer": "^10.4.4", 44 | "eslint": "8.13.0", 45 | "eslint-config-next": "12.1.5", 46 | "postcss": "^8.4.12", 47 | "tailwindcss": "^3.0.24", 48 | "typescript": "4.6.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pages/api/auth/me.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | 3 | import User from "@db/models/User" 4 | import { withUser } from "@middlewares" 5 | import { ApiHandler, Request } from "@types" 6 | 7 | const handler: ApiHandler = async (req, res) => { 8 | if (req.method !== "GET") { 9 | res.status(405).end() 10 | return 11 | } 12 | 13 | try { 14 | const fifteenMins = 1000 * 60 * 15 15 | const lastUpdatedUser = moment(req.user.updatedAt as string) 16 | 17 | // if fifteen minute is passed since the last update 18 | // then fetch the user from the twitter api and update the user 19 | const isPassed = moment().diff(lastUpdatedUser) > fifteenMins 20 | 21 | if (isPassed) { 22 | const user = await getUserFromTwitter(req) 23 | res.json(user) 24 | } else { 25 | res.json(req.user) 26 | } 27 | } catch (error) { 28 | res.status(500).json({ message: "internal server error." }) 29 | } 30 | } 31 | 32 | const getUserFromTwitter = async (req: Request) => { 33 | const currentUser = await req.client.currentUser() 34 | 35 | const user = await User.findOneAndUpdate( 36 | { 37 | twitter_user_id: currentUser.id, 38 | }, 39 | { 40 | username: currentUser.screen_name, 41 | profile_picture: currentUser.profile_image_url_https, 42 | }, 43 | { 44 | upsert: true, 45 | new: true, 46 | } 47 | ).select("-twitter_access_token -twitter_access_secret") 48 | 49 | return user 50 | } 51 | 52 | export default withUser(handler) 53 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { Toaster } from 'react-hot-toast' 4 | import { FC, ReactNode, useEffect } from 'react' 5 | import Router from 'next/router' 6 | 7 | import { useLoading, useUser, client } from '@lib' 8 | import { Loader } from '@components' 9 | 10 | function MyApp({ Component, pageProps }: AppProps) { 11 | return ( 12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | ) 20 | } 21 | 22 | // initiates the user state 23 | const UserProvider: FC<{ children: ReactNode; pageProps: any }> = ({ 24 | children, 25 | pageProps, 26 | }) => { 27 | const { setLoading, loading } = useLoading() 28 | const { user, setUser } = useUser() 29 | 30 | useEffect(() => { 31 | if (user) return 32 | 33 | setLoading(true) 34 | client 35 | .get('/api/auth/me') 36 | .then((r) => { 37 | setUser(r.data) 38 | }) 39 | .catch((e) => {}) 40 | .finally(() => { 41 | setLoading(false) 42 | }) 43 | }, []) // eslint-disable-line react-hooks/exhaustive-deps 44 | 45 | useEffect(() => { 46 | if (pageProps.protected && !user && !loading) { 47 | Router.push('/') 48 | } 49 | }, [user, loading]) // eslint-disable-line react-hooks/exhaustive-deps 50 | 51 | if (pageProps.protected && !user) return 52 | 53 | return <>{children} 54 | } 55 | 56 | export default MyApp 57 | -------------------------------------------------------------------------------- /components/layout/popularCheatsheets/SmallCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { FC } from 'react' 4 | import { FaThumbsUp } from 'react-icons/fa' 5 | import { ISmallCardProps } from '.' 6 | 7 | export const SmallCard: FC = ({ cheatsheet }) => { 8 | return ( 9 |
10 | 11 | 12 |

13 | {cheatsheet.name} 14 |

15 |
16 | 17 | 18 | 19 | 20 |
21 | {cheatsheet.author.name} 28 |

29 | By {cheatsheet.author.name?.split(' ')[0]} 30 |

31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |

{cheatsheet.likes}

39 |
40 | 41 |
42 | {cheatsheet.views} Views 43 |
44 | 45 |
46 | {cheatsheet.cards.length} Cards 47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /components/pages/dashboard/CheatsheetItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Router from 'next/router' 3 | import { FC } from 'react' 4 | import { AiTwotoneLike } from 'react-icons/ai' 5 | import { FaEdit, FaTrash } from 'react-icons/fa' 6 | import { ICheatsheetItemProps } from '.' 7 | 8 | export const CheatsheetItem: FC = ({ 9 | cheatsheet, 10 | deleteHandler, 11 | }) => { 12 | return ( 13 |
14 |
15 | 16 | 17 |

18 | {cheatsheet.name} 19 |

20 |
21 | 22 | 23 |
24 | deleteHandler(cheatsheet._id)} 26 | className="bg-gray-200 hover:bg-gray-100 p-2 rounded-md cursor-pointer" 27 | /> 28 | 29 | { 31 | Router.push(`/cheatsheets/edit/${cheatsheet._id}`) 32 | }} 33 | className="bg-gray-200 hover:bg-gray-100 p-2 rounded-md cursor-pointer" 34 | /> 35 |
36 |
37 | 38 |
39 |
40 | 41 |

{cheatsheet.likes}

42 |
43 |

{cheatsheet.views} views

44 |

45 | {cheatsheet.cards.length} Card 46 | {cheatsheet.cards.length === 1 ? ' ' : 's'} 47 |

48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Cheatly 2 | 3 | [Cheatly](https://cheatly.vercel.app/) is a simple cheatsheet builder to help you create cheatsheets to study. You can also share the cheatsheets you make with your audience. 4 | 5 | Cheatly is made with Next.js and TypeScript. 6 | 7 | ## Installation 8 | **Environment variables** 9 | - [MONGO_URI](https://www.mongodb.com/docs/guides/server/drivers/) 10 | - [TWITTER_API_KEY](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api) 11 | - [TWITTER_API_SECRET](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api) 12 | - [TWITTER_ACCESS_TOKEN](https://developer.twitter.com/en/docs/authentication/oauth-1-0a/obtaining-user-access-tokens) 13 | - [TWITTER_ACCESS_SECRET](https://developer.twitter.com/en/docs/authentication/oauth-1-0a/obtaining-user-access-tokens) 14 | - SERVER_URL (the server url in your case it would be http://localhost:3000) 15 | - CLIENT_URL (the client url in your case it would be http://localhost:3000) 16 | 17 | To install the app, 18 | 19 | - Clone Cheatly repository: 20 | 21 | ``` bash 22 | git clone https://github.com/Dawsoncodes/cheatly 23 | ``` 24 | 25 | - Install the dependencies: 26 | 27 | ```bash 28 | yarn 29 | ``` 30 | 31 | - Build the app 32 | 33 | ```bash 34 | yarn build 35 | ``` 36 | 37 | - Run the app 38 | 39 | ```bash 40 | yarn start 41 | ``` 42 | 43 | - Run the development environment with: 44 | 45 | ```bash 46 | yarn dev 47 | ``` 48 | 49 | Now you can tinker with Cheatly. 😃 50 | ## Contributions 51 | Feel free to contribute to Cheatly. If you want to report a bug or make suggestions for improvement, raise an [issue](https://github.com/Dawsoncodes/cheatly/issues/new). 52 | 53 | If you're new to making contributions, please read our Contributions guide. 54 | 55 | While contributing, make sure to adhere to the Code of Conduct. 56 | 57 | Thank you for checking Cheatly out. 😊 58 | -------------------------------------------------------------------------------- /pages/api/cheatsheets/[id]/index.ts: -------------------------------------------------------------------------------- 1 | import CheatSheet from "@db/models/Cheatsheet" 2 | import View from "@db/models/View" 3 | import { withUser } from "@middlewares" 4 | import { ApiHandler } from "@types" 5 | 6 | const PUT: ApiHandler = async (req, res) => { 7 | let { name, cards } = req.body 8 | 9 | try { 10 | if (!name || !cards) { 11 | throw new Error("Missing required fields") 12 | } 13 | 14 | cards = cards.filter((card: string) => card) 15 | 16 | if (!cards.length) throw Error("No cards provided") 17 | 18 | cards = cards.map((card: string) => ({ content: card })) 19 | 20 | const cheatsheet = await CheatSheet.findOneAndUpdate( 21 | { 22 | _id: req.query.id, 23 | user_id: req.user._id, 24 | }, 25 | { 26 | name, 27 | cards, 28 | }, 29 | { 30 | new: true, 31 | } 32 | ) 33 | 34 | res.json(cheatsheet) 35 | } catch (error) { 36 | res.status(400).json({ 37 | message: "Something went wrong", 38 | }) 39 | } 40 | } 41 | 42 | const GET: ApiHandler = async (req, res) => { 43 | try { 44 | const { id } = req.query 45 | if (!id) throw Error() 46 | 47 | const [cheatsheet, views] = await Promise.all([ 48 | CheatSheet.findOne({ _id: id }).lean(), 49 | View.countDocuments({ cheatsheet_id: id }), 50 | ]) 51 | 52 | res.json({ 53 | ...cheatsheet, 54 | views, 55 | }) 56 | } catch (error) { 57 | res.json({ 58 | message: "Something went wrong", 59 | }) 60 | } 61 | } 62 | 63 | const DELETE: ApiHandler = async (req, res) => { 64 | try { 65 | await CheatSheet.deleteOne({ 66 | _id: req.query.id, 67 | }) 68 | 69 | res.json({ 70 | message: "Cheatsheet deleted", 71 | }) 72 | } catch (error) { 73 | res.status(400).json({ 74 | message: "Something went wrong", 75 | }) 76 | } 77 | } 78 | 79 | const handlers = { 80 | PUT, 81 | GET, 82 | DELETE, 83 | } 84 | 85 | const handler: ApiHandler = async (req, res) => { 86 | const handler = handlers[req.method as keyof typeof handlers] 87 | 88 | if (handler) { 89 | await handler(req, res) 90 | return 91 | } 92 | 93 | res.status(405).send({ 94 | error: "Method not allowed", 95 | }) 96 | } 97 | 98 | export default withUser(handler) 99 | -------------------------------------------------------------------------------- /lib/store.ts: -------------------------------------------------------------------------------- 1 | import { UserType } from "../db/models/User" 2 | import create from "zustand" 3 | import { CheatsheetType } from "../db/models/Cheatsheet" 4 | 5 | export interface CheatsheetWLikesAndViews extends CheatsheetType { 6 | likes: number 7 | views: number 8 | } 9 | 10 | interface IStore { 11 | user: UserType | null 12 | setUser: (user: UserType | null) => void 13 | 14 | loading: boolean 15 | setLoading: (loading: boolean) => void 16 | 17 | myCheatsheets: CheatsheetWLikesAndViews[] 18 | setMyCheatsheets: (cheatsheets: CheatsheetWLikesAndViews[]) => void 19 | 20 | popularCheatsheets: (CheatsheetWLikesAndViews & { 21 | author: Partial 22 | })[] 23 | setPopularCheatsheets: ( 24 | popular: (CheatsheetWLikesAndViews & { author: Partial })[] 25 | ) => void 26 | } 27 | 28 | export const useStore = create((set) => ({ 29 | user: null, 30 | setUser: (user) => set({ user }), 31 | 32 | loading: true, 33 | setLoading: (loading) => set({ loading }), 34 | 35 | myCheatsheets: [], 36 | setMyCheatsheets: (cheatsheets) => set({ myCheatsheets: cheatsheets }), 37 | 38 | popularCheatsheets: [], 39 | setPopularCheatsheets: (popular) => set({ popularCheatsheets: popular }), 40 | })) 41 | 42 | export const useUser = () => { 43 | const { user, setUser } = useStore((state) => ({ 44 | user: state.user, 45 | setUser: state.setUser, 46 | })) 47 | 48 | return { 49 | user, 50 | setUser, 51 | } 52 | } 53 | 54 | export const useLoading = () => { 55 | const { loading, setLoading } = useStore((state) => ({ 56 | loading: state.loading, 57 | setLoading: state.setLoading, 58 | })) 59 | 60 | return { 61 | loading, 62 | setLoading, 63 | } 64 | } 65 | 66 | export const useMyCheatsheets = () => { 67 | const { myCheatsheets, setMyCheatsheets } = useStore((state) => ({ 68 | myCheatsheets: state.myCheatsheets, 69 | setMyCheatsheets: state.setMyCheatsheets, 70 | })) 71 | 72 | return { 73 | myCheatsheets, 74 | setMyCheatsheets, 75 | } 76 | } 77 | 78 | export const usePopularCheatsheets = () => { 79 | const { popularCheatsheets, setPopularCheatsheets } = useStore((state) => ({ 80 | popularCheatsheets: state.popularCheatsheets, 81 | setPopularCheatsheets: state.setPopularCheatsheets, 82 | })) 83 | 84 | return { 85 | popularCheatsheets, 86 | setPopularCheatsheets, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pages/api/auth/twitter/authorize.ts: -------------------------------------------------------------------------------- 1 | import TwitterApi from 'twitter-api-v2' 2 | import { serialize } from 'cookie' 3 | 4 | import User from '@db/models/User' 5 | import { genToken } from '@lib' 6 | import { ApiHandler } from '@types' 7 | import dayjs from 'dayjs' 8 | 9 | const handler: ApiHandler = async (req, res) => { 10 | if (req.method !== 'GET') { 11 | res.status(405).end() 12 | return 13 | } 14 | // 15 | 16 | await GET(req, res) 17 | } 18 | 19 | const GET: ApiHandler = async (req, res) => { 20 | const { oauth_token, oauth_verifier } = req.query 21 | 22 | try { 23 | if ( 24 | !oauth_verifier || 25 | typeof oauth_token !== 'string' || 26 | typeof oauth_verifier !== 'string' 27 | ) 28 | return res.status(400).json({ message: 'Error' }) 29 | 30 | const client = new TwitterApi({ 31 | appKey: process.env.TWITTER_API_KEY as string, 32 | appSecret: process.env.TWITTER_API_SECRET as string, 33 | accessToken: oauth_token, 34 | accessSecret: process.env.TWITTER_ACCESS_TOKEN as string, 35 | }) 36 | 37 | const result = await client.login(oauth_verifier) 38 | 39 | if (!result) throw Error('Invalid request!') 40 | 41 | const userClient = new TwitterApi({ 42 | appKey: process.env.TWITTER_API_KEY as string, 43 | appSecret: process.env.TWITTER_API_SECRET as string, 44 | accessToken: result.accessToken, 45 | accessSecret: result.accessSecret, 46 | }) 47 | 48 | const userData = await userClient.v1.verifyCredentials({ 49 | include_email: true, 50 | }) 51 | 52 | const user = await User.findOneAndUpdate( 53 | { 54 | twitter_user_id: result.userId, 55 | }, 56 | { 57 | username: userData.screen_name, 58 | email: userData.email, 59 | twitter_access_token: result.accessToken, 60 | twitter_access_secret: result.accessSecret, 61 | profile_picture: userData.profile_image_url_https, 62 | name: userData.name, 63 | }, 64 | { 65 | upsert: true, 66 | new: true, 67 | } 68 | ) 69 | 70 | const token = genToken(user._id) 71 | 72 | if (!token) throw Error('Internal server error!') 73 | 74 | res 75 | .setHeader( 76 | 'Set-Cookie', 77 | serialize('token', token, { 78 | expires: dayjs().add(30, 'days').toDate(), 79 | path: '/', 80 | }) 81 | ) 82 | .redirect(`${process.env.CLIENT_URL}/dashboard`) 83 | } catch (error) { 84 | if (error instanceof Error) 85 | res.status(500).json({ message: 'Internal server error.' }) 86 | } 87 | } 88 | 89 | export default handler 90 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticProps } from 'next' 2 | import Head from 'next/head' 3 | import Image from 'next/image' 4 | import Router from 'next/router' 5 | import { FC } from 'react' 6 | 7 | import CheatSheet from '@db/models/Cheatsheet' 8 | import User from '@db/models/User' 9 | import { useUser } from '@lib' 10 | import { 11 | PrimaryButton, 12 | Container, 13 | IHomeProps, 14 | H1, 15 | H2, 16 | H3, 17 | Footer, 18 | PopularCheatsheets, 19 | } from '@components' 20 | 21 | const desc = 22 | 'Cheatly is a simple cheatsheet generator. Create cheatsheets and share them with your audience.' 23 | 24 | const title = 'Cheatly | A simple cheatsheet generator' 25 | 26 | const link = 'https://cheatly.vercel.app' 27 | 28 | const siteName = 'Cheatly' 29 | 30 | const image = 'https://i.ibb.co/Wx91dNP/cheatly.png' 31 | 32 | const color = '#F7F7F7' 33 | 34 | const Home: FC = ({ cheatsheetCount, userCount }) => { 35 | const { user } = useUser() 36 | 37 | const createCheatsheetHandler = () => { 38 | if (user) return Router.push('/dashboard') 39 | 40 | Router.push('/api/auth/twitter/login') 41 | } 42 | 43 | return ( 44 | <> 45 | 46 | {title} 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | cheatly logo 73 |

Cheatly

74 |

75 | Create a cheatsheet, and share it with your audience 76 |

77 | 78 |
79 | 80 | {cheatsheetCount} cheatsheets{' '} 81 | 82 | are hosted on Cheatly{' '} 83 | 84 | by {userCount} {userCount === 1 ? 'user' : 'users'}. 85 | 86 |
87 | 88 | Create a cheatsheet 89 | 90 |
91 | 92 |

93 | Create your cheatsheet easily 94 |

95 |

96 | Creating a cheatsheet is just a few clicks away. 97 |

98 | 99 |

100 | Share your cheatsheet 101 |

102 |

103 | You create a cheatsheet, and you get a link, that's how easy it 104 | is to share your cheatsheets. 105 |

106 |
107 |
108 | 109 | 110 |