├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── server.yaml │ └── web.yaml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── SECURITY.md ├── assets ├── banner.png ├── dark_mode.png ├── editor.png ├── main.png ├── menu.png ├── modal.png ├── quickfind.png └── sidebar.png ├── lerna.json ├── package.json ├── server ├── .dockerignore ├── .env.example ├── Dockerfile ├── ormconfig.json ├── package.json ├── src │ ├── constants.ts │ ├── entities │ │ ├── Folder.ts │ │ ├── Note.ts │ │ └── User.ts │ ├── env.d.ts │ ├── index.ts │ ├── middleware │ │ └── isAuth.ts │ ├── migrations │ │ └── 1638362548353-Initial.ts │ ├── resolvers │ │ ├── folder.ts │ │ ├── note.ts │ │ ├── upload │ │ │ └── avatar.ts │ │ └── user.ts │ ├── schemas │ │ └── UserInput.ts │ ├── types.ts │ └── utils │ │ ├── chooseRandomFolderColor.ts │ │ ├── sendEmail.ts │ │ └── validateRegister.ts ├── tsconfig.json └── yarn.lock └── web ├── .eslintrc.json ├── .gitignore ├── README.md ├── codegen.yml ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── src ├── components │ ├── shared │ │ ├── Meta.tsx │ │ ├── Sidebar.tsx │ │ ├── Spinner.tsx │ │ └── Wrapper.tsx │ ├── showcase │ │ └── Navbar.tsx │ └── ui │ │ ├── ColoredCircle.tsx │ │ ├── InputField.tsx │ │ ├── Navbar.tsx │ │ ├── Welcome.tsx │ │ ├── cards │ │ ├── Folder.tsx │ │ └── Note.tsx │ │ ├── editor │ │ ├── core │ │ │ ├── navbar │ │ │ │ ├── Icon.tsx │ │ │ │ └── ParaStyleDropdown.tsx │ │ │ ├── renderElement.tsx │ │ │ └── renderLeaf.tsx │ │ └── editor.tsx │ │ └── modals │ │ ├── AddImage.tsx │ │ ├── DeleteNote.tsx │ │ ├── Folder.tsx │ │ ├── QuickFind.tsx │ │ └── Settings.tsx ├── generated │ └── graphql.tsx ├── graphql │ ├── fragments │ │ ├── RegularError.graphql │ │ ├── RegularFolder.graphql │ │ ├── RegularNote.graphql │ │ ├── RegularUser.graphql │ │ └── RegularUserResponse.graphql │ ├── mutations │ │ ├── addNoteToFolder.graphql │ │ ├── changePassword.graphql │ │ ├── createFolder.graphql │ │ ├── createNote.graphql │ │ ├── deleteFolder.graphql │ │ ├── deleteNote.graphql │ │ ├── deleteNoteFromFolder.graphql │ │ ├── forgotPassword.graphql │ │ ├── login.graphql │ │ ├── logout.graphql │ │ ├── register.graphql │ │ ├── updateName.graphql │ │ ├── updateNote.graphql │ │ └── updateNoteTitle.graphql │ └── queries │ │ ├── getNote.graphql │ │ └── me.graphql ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── hello.ts │ ├── app │ │ ├── index.tsx │ │ └── n │ │ │ └── [id].tsx │ ├── changepass │ │ └── [id].tsx │ ├── forgotpass.tsx │ ├── index.tsx │ ├── login.tsx │ ├── product.tsx │ └── register.tsx ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ ├── Home.module.css │ └── globals.css └── utils │ ├── FindFolderId.ts │ ├── findNoteFolder.ts │ ├── searchNotes.ts │ ├── timeSince.ts │ ├── toErrorMap.ts │ └── useIsAuth.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/.github/CODEOWNERS -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [japrozs] 4 | patreon: JaprozSaini 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://japrozsaini.me'] 13 | -------------------------------------------------------------------------------- /.github/workflows/server.yaml: -------------------------------------------------------------------------------- 1 | name: Build server 2 | 3 | on: 4 | push: 5 | paths: 6 | - "server/**" 7 | 8 | jobs: 9 | server: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: cd server && yarn && yarn build 17 | -------------------------------------------------------------------------------- /.github/workflows/web.yaml: -------------------------------------------------------------------------------- 1 | name: Build web 2 | 3 | on: 4 | push: 5 | paths: 6 | - "web/**" 7 | 8 | jobs: 9 | web: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: cd web && yarn add -D typescript && yarn install && yarn lint 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.excludeLanguages": [ 3 | "typescriptreact" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2021 Japroz Singh Saini 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](assets/banner.png) 2 | 3 | # Dino 4 | 5 | Dino is a new website, mobile app and desktop app to jot down your thoughts and all the stuff that you want to access quickly and easily. Cos' not everything's about being productive. `Dino is a work in progress` 6 | 7 | Dino - A new way to jot down your thoughts | Product Hunt 8 | 9 | ## Current status of dino 10 | 11 | ![Screenshot](assets/main.png) 12 | ![Screenshot](assets/menu.png) 13 | ![Screenshot](assets/modal.png) 14 | ![Screenshot](assets/editor.png) 15 | ![Screenshot](assets/quickfind.png) 16 | ![Screenshot](assets/sidebar.png) 17 | ![Screenshot](assets/dark_mode.png) 18 | 19 | ## Features 20 | 21 | - [x] A simple to use note taking experience 22 | - [x] Folders to organise notes in a concise, easy and well-understoof manner 23 | - [ ] A storage space like google cloud, dropbox so of around `5Gb` to access your files alongside your notes 24 | 25 | ## Contributing 26 | 27 | ALl help is appreciated. If you want to contribute to `dino`, pls make a pull request and I'll review it. 28 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | DM me on https://twitter.com/japrozss or email me at `sainijaproz@gmail.com` -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/banner.png -------------------------------------------------------------------------------- /assets/dark_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/dark_mode.png -------------------------------------------------------------------------------- /assets/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/editor.png -------------------------------------------------------------------------------- /assets/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/main.png -------------------------------------------------------------------------------- /assets/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/menu.png -------------------------------------------------------------------------------- /assets/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/modal.png -------------------------------------------------------------------------------- /assets/quickfind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/quickfind.png -------------------------------------------------------------------------------- /assets/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/assets/sidebar.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["server"], 3 | "npmClient": "yarn", 4 | "version": "0.0.0" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "lerna": "^4.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-debug.log -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | REDIS_URL= 3 | PORT= 4 | SESSION_SECRET= 5 | CORS_ORIGIN= 6 | DO_ENDPOINT= 7 | DO_ACCESS_KEY_ID= 8 | DO_SECRET_ACCESS_KEY= -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json ./ 6 | COPY yarn.lock ./ 7 | 8 | RUN yarn 9 | 10 | COPY . . 11 | COPY .env.production .env 12 | 13 | RUN yarn build 14 | 15 | 16 | ENV NODE_ENV production 17 | 18 | EXPOSE 8080 19 | CMD ["node", "dist/index.js"] 20 | USER node -------------------------------------------------------------------------------- /server/ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "postgres", 3 | "host": "localhost", 4 | "port": 5432, 5 | "username": "postgres", 6 | "password": "postgres", 7 | "database": "dino2", 8 | "entities": ["dist/entities/*.js"], 9 | "migrations": ["dist/migrations/*.js"] 10 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "gen-env": "gen-env-types .env -o src/env.d.ts -e .", 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "dev": "nodemon dist/index.js", 11 | "start": "node dist/index.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "apollo-server-express": "2.25.0", 18 | "aws-sdk": "^2.1040.0", 19 | "connect-redis": "^6.0.0", 20 | "cors": "^2.8.5", 21 | "dotenv-safe": "^8.2.0", 22 | "express": "^4.17.1", 23 | "express-session": "^1.17.2", 24 | "graphql": "^15.6.1", 25 | "graphql-type-json": "^0.3.2", 26 | "ioredis": "^4.27.11", 27 | "multer": "^1.4.3", 28 | "multer-s3": "^2.10.0", 29 | "pg": "^8.7.1", 30 | "type-graphql": "^1.1.1", 31 | "typeorm": "^0.2.38" 32 | }, 33 | "devDependencies": { 34 | "@types/connect-redis": "^0.0.17", 35 | "@types/express-session": "^1.17.4", 36 | "@types/graphql-type-json": "^0.3.2", 37 | "@types/ioredis": "^4.27.6", 38 | "@types/multer": "^1.4.7", 39 | "@types/multer-s3": "^2.7.11", 40 | "@types/node": "^16.11.8", 41 | "@types/nodemailer": "^6.4.4", 42 | "@types/uuid": "^8.3.1", 43 | "argon2": "^0.28.2", 44 | "gen-env-types": "^1.3.0", 45 | "nodemailer": "^6.6.5", 46 | "typescript": "^4.5.2", 47 | "uuid": "^8.3.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const __prod__ = process.env.NODE_ENV === "production"; 2 | export const COOKIE_NAME = "qid"; 3 | export const FORGET_PASSWORD_PREFIX = "forget-password:"; 4 | -------------------------------------------------------------------------------- /server/src/entities/Folder.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | CreateDateColumn, 4 | BaseEntity, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | Column, 9 | ManyToOne, 10 | } from "typeorm"; 11 | import { User } from "./User"; 12 | 13 | @ObjectType() 14 | @Entity() 15 | export class Folder extends BaseEntity { 16 | @Field() 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Field() 21 | @Column() 22 | name: string; 23 | 24 | @Field() 25 | @Column() 26 | color: string; 27 | 28 | @Field(() => [String]) 29 | @Column("simple-array") 30 | noteIds: string[]; 31 | 32 | @Field(() => User) 33 | @ManyToOne(() => User, (user) => user.folders) 34 | creator: User; 35 | 36 | @Field() 37 | @Column() 38 | creatorId: number; 39 | 40 | @Field(() => String) 41 | @CreateDateColumn() 42 | createdAt: Date; 43 | 44 | @Field(() => String) 45 | @UpdateDateColumn() 46 | updatedAt: Date; 47 | } 48 | -------------------------------------------------------------------------------- /server/src/entities/Note.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | CreateDateColumn, 4 | BaseEntity, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | Column, 9 | ManyToOne, 10 | } from "typeorm"; 11 | import { User } from "./User"; 12 | 13 | @ObjectType() 14 | @Entity() 15 | export class Note extends BaseEntity { 16 | @Field() 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Field() 21 | @Column() 22 | title: string; 23 | 24 | @Field(() => String) 25 | @Column({ nullable: true }) 26 | body: string; 27 | 28 | @Field(() => String) 29 | @Column({ default: "active" }) 30 | status: string; 31 | 32 | @Field(() => User) 33 | @ManyToOne(() => User, (user) => user.notes) 34 | creator: User; 35 | 36 | @Field() 37 | @Column() 38 | creatorId: number; 39 | 40 | @Field(() => String) 41 | @CreateDateColumn() 42 | createdAt: Date; 43 | 44 | @Field(() => String) 45 | @UpdateDateColumn() 46 | updatedAt: Date; 47 | } 48 | -------------------------------------------------------------------------------- /server/src/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | BaseEntity, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | OneToMany, 10 | } from "typeorm"; 11 | import { Folder } from "./Folder"; 12 | import { Note } from "./Note"; 13 | 14 | @ObjectType() 15 | @Entity() 16 | export class User extends BaseEntity { 17 | @Field() 18 | @PrimaryGeneratedColumn() 19 | id: number; 20 | 21 | @Field() 22 | @Column() 23 | name: string; 24 | 25 | @Field() 26 | @Column({ default: "light" }) 27 | theme: string; 28 | 29 | @Field() 30 | @Column({ 31 | default: 32 | "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y", 33 | }) 34 | imgUrl: string; 35 | 36 | @Field() 37 | @Column({ unique: true }) 38 | email: string; 39 | 40 | @Column() 41 | password!: string; 42 | 43 | @Field(() => [Note]) 44 | @OneToMany(() => Note, (note) => note.creator) 45 | notes: Note[]; 46 | 47 | @Field(() => [Folder]) 48 | @OneToMany(() => Folder, (folder) => folder.creator) 49 | folders: Folder[]; 50 | 51 | @Field(() => String) 52 | @CreateDateColumn() 53 | createdAt: Date; 54 | 55 | @Field(() => String) 56 | @UpdateDateColumn() 57 | updatedAt: Date; 58 | } 59 | -------------------------------------------------------------------------------- /server/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | DATABASE_URL: string; 4 | REDIS_URL: string; 5 | PORT: string; 6 | SESSION_SECRET: string; 7 | CORS_ORIGIN: string; 8 | DO_ENDPOINT: string; 9 | DO_ACCESS_KEY_ID: string; 10 | DO_SECRET_ACCESS_KEY: string; 11 | } 12 | } -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "dotenv-safe/config"; 3 | import { __prod__, COOKIE_NAME } from "./constants"; 4 | import express from "express"; 5 | import { ApolloServer } from "apollo-server-express"; 6 | import { buildSchema } from "type-graphql"; 7 | import { UserResolver } from "./resolvers/user"; 8 | import Redis from "ioredis"; 9 | import session from "express-session"; 10 | import connectRedis from "connect-redis"; 11 | import cors from "cors"; 12 | import { createConnection } from "typeorm"; 13 | import { User } from "./entities/User"; 14 | import { Note } from "./entities/Note"; 15 | import path from "path"; 16 | import { NoteResolver } from "./resolvers/note"; 17 | import { Folder } from "./entities/Folder"; 18 | import { FolderResolver } from "./resolvers/folder"; 19 | import avatarUpload from "./resolvers/upload/avatar"; 20 | 21 | const main = async () => { 22 | const conn = await createConnection({ 23 | type: "postgres", 24 | url: process.env.DATABASE_URL, 25 | logging: true, 26 | synchronize: true, 27 | migrations: [path.join(__dirname, "./migrations/*")], 28 | entities: [User, Note, Folder], 29 | }); 30 | await conn.runMigrations(); 31 | 32 | const app = express(); 33 | 34 | const RedisStore = connectRedis(session); 35 | const redis = new Redis(process.env.REDIS_URL); 36 | app.set("trust proxy", 1); 37 | app.use( 38 | cors({ 39 | origin: process.env.CORS_ORIGIN, 40 | credentials: true, 41 | }) 42 | ); 43 | app.use( 44 | session({ 45 | name: COOKIE_NAME, 46 | store: new RedisStore({ 47 | client: redis, 48 | disableTouch: true, 49 | }), 50 | cookie: { 51 | maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years 52 | httpOnly: true, 53 | sameSite: "lax", 54 | secure: __prod__, 55 | domain: __prod__ ? ".japrozsaini.me" : undefined, 56 | }, 57 | saveUninitialized: false, 58 | secret: process.env.SESSION_SECRET, 59 | resave: false, 60 | }) 61 | ); 62 | 63 | const apolloServer = new ApolloServer({ 64 | schema: await buildSchema({ 65 | resolvers: [UserResolver, NoteResolver, FolderResolver], 66 | validate: false, 67 | }), 68 | context: ({ req, res }) => ({ 69 | req, 70 | res, 71 | redis, 72 | }), 73 | }); 74 | 75 | app.use("/upload/", avatarUpload); 76 | 77 | apolloServer.applyMiddleware({ 78 | app, 79 | cors: false, 80 | }); 81 | 82 | app.listen(parseInt(process.env.PORT), () => { 83 | console.log(`🚀 Server started on localhost:${process.env.PORT}`); 84 | }); 85 | }; 86 | 87 | main().catch((err) => { 88 | console.error(err); 89 | }); 90 | -------------------------------------------------------------------------------- /server/src/middleware/isAuth.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../types"; 2 | import { MiddlewareFn } from "type-graphql"; 3 | 4 | export const isAuth: MiddlewareFn = ({ context }, next) => { 5 | if (!context.req.session.userId) { 6 | throw new Error("not authenticated"); 7 | } 8 | 9 | return next(); 10 | }; 11 | 12 | 13 | export const expressIsAuth = (req: any, _res: any, next: any) => { 14 | if (!req.session.userId) { 15 | throw new Error("not authenticated"); 16 | } 17 | return next(); 18 | }; -------------------------------------------------------------------------------- /server/src/migrations/1638362548353-Initial.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class Initial1638362548353 implements MigrationInterface { 4 | name = 'Initial1638362548353' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TABLE "note" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "body" character varying, "status" character varying NOT NULL DEFAULT 'active', "creatorId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_96d0c172a4fba276b1bbed43058" PRIMARY KEY ("id"))`); 8 | await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "theme" character varying NOT NULL DEFAULT 'light', "imgUrl" character varying NOT NULL DEFAULT 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y', "email" character varying NOT NULL, "password" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); 9 | await queryRunner.query(`CREATE TABLE "folder" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "color" character varying NOT NULL, "noteIds" text NOT NULL, "creatorId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6278a41a706740c94c02e288df8" PRIMARY KEY ("id"))`); 10 | await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_46c222669def9088455c5f6f2b8" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); 11 | await queryRunner.query(`ALTER TABLE "folder" ADD CONSTRAINT "FK_4d6ef3409b06099753ed80f08f9" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`ALTER TABLE "folder" DROP CONSTRAINT "FK_4d6ef3409b06099753ed80f08f9"`); 16 | await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_46c222669def9088455c5f6f2b8"`); 17 | await queryRunner.query(`DROP TABLE "folder"`); 18 | await queryRunner.query(`DROP TABLE "user"`); 19 | await queryRunner.query(`DROP TABLE "note"`); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /server/src/resolvers/folder.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from "../entities/Folder"; 2 | import { isAuth } from "../middleware/isAuth"; 3 | import { Context } from "../types"; 4 | import { Arg, Ctx, Int, Mutation, UseMiddleware } from "type-graphql"; 5 | import { chooseRandomFolderColor } from "../utils/chooseRandomFolderColor"; 6 | 7 | export class FolderResolver { 8 | @UseMiddleware(isAuth) 9 | @Mutation(() => Boolean) 10 | async createFolder(@Arg("name") name: string, @Ctx() { req }: Context) { 11 | if (name.trim().length == 0) { 12 | return false; 13 | } 14 | await Folder.create({ 15 | name, 16 | noteIds: [], 17 | creatorId: req.session.userId, 18 | color: chooseRandomFolderColor(), 19 | }).save(); 20 | return true; 21 | } 22 | 23 | @UseMiddleware(isAuth) 24 | @Mutation(() => Boolean) 25 | async addNoteToFolder( 26 | @Arg("folderId", () => Int) folderId: number, 27 | @Arg("noteId", () => Int) noteId: number 28 | ) { 29 | const fold = await Folder.findOne(folderId); 30 | if (fold?.noteIds.includes(noteId.toString())) { 31 | return true; 32 | } 33 | await Folder.update( 34 | { id: folderId }, 35 | { 36 | noteIds: [noteId.toString(), ...(fold?.noteIds as string[])], 37 | } 38 | ); 39 | return true; 40 | } 41 | 42 | @UseMiddleware(isAuth) 43 | @Mutation(() => Boolean) 44 | async deleteNoteFromFolder( 45 | @Arg("folderId", () => Int) folderId: number, 46 | @Arg("noteId", () => Int) noteId: number, 47 | @Ctx() { req }: Context 48 | ) { 49 | const folder = await Folder.findOne(folderId, { 50 | relations: ["creator"], 51 | }); 52 | if (folder?.creator.id != req.session.userId) { 53 | return false; 54 | } 55 | 56 | let arr = folder?.noteIds; 57 | let i = arr?.indexOf(noteId.toString()); 58 | console.log(arr); 59 | if (i == -1) { 60 | return false; 61 | } 62 | arr?.splice(i as number, 1); 63 | console.log(arr); 64 | 65 | await Folder.update(folderId, { 66 | noteIds: arr, 67 | }); 68 | 69 | return true; 70 | } 71 | 72 | @UseMiddleware(isAuth) 73 | @Mutation(() => Boolean) 74 | async deleteFolder( 75 | @Arg("id", () => Int) id: number, 76 | @Ctx() { req }: Context 77 | ) { 78 | const fold = await Folder.findOne(id, { relations: ["creator"] }); 79 | if (fold?.creator.id != req.session.userId) { 80 | return false; 81 | } 82 | 83 | await Folder.delete({ id }); 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/src/resolvers/note.ts: -------------------------------------------------------------------------------- 1 | import { isAuth } from "../middleware/isAuth"; 2 | import { Context } from "../types"; 3 | import { Arg, Ctx, Int, Mutation, Query, UseMiddleware } from "type-graphql"; 4 | import { Note } from "../entities/Note"; 5 | 6 | export class NoteResolver { 7 | @UseMiddleware(isAuth) 8 | @Mutation(() => Note) 9 | async createNote(@Arg("title") title: string, @Ctx() { req }: Context) { 10 | if (title.trim().length == 0) { 11 | return false; 12 | } 13 | return Note.create({ 14 | title, 15 | creatorId: req.session.userId, 16 | body: "", 17 | }).save(); 18 | } 19 | 20 | @UseMiddleware(isAuth) 21 | @Query(() => Note) 22 | async getNote(@Arg("id", () => Int) id: number) { 23 | return Note.findOne({ where: { id } }); 24 | } 25 | 26 | @UseMiddleware(isAuth) 27 | @Mutation(() => Boolean) 28 | async updateNote( 29 | @Arg("id", () => Int!) id: number, 30 | @Arg("body") body: string, 31 | @Ctx() { req }: Context 32 | ) { 33 | const note = await Note.findOne(id, { relations: ["creator"] }); 34 | if (note?.creator.id != req.session.userId) { 35 | return false; 36 | } 37 | 38 | await Note.update(id, { 39 | body, 40 | }); 41 | return true; 42 | } 43 | 44 | @UseMiddleware(isAuth) 45 | @Mutation(() => Boolean) 46 | async updateNoteTitle( 47 | @Arg("id", () => Int!) id: number, 48 | @Arg("title") title: string, 49 | @Ctx() { req }: Context 50 | ) { 51 | const note = await Note.findOne(id, { relations: ["creator"] }); 52 | if (note?.creator.id != req.session.userId) { 53 | return false; 54 | } 55 | 56 | await Note.update(id, { 57 | title, 58 | }); 59 | return true; 60 | } 61 | 62 | @UseMiddleware(isAuth) 63 | @Mutation(() => Boolean) 64 | async deleteNote( 65 | @Arg("id", () => Int) id: number, 66 | @Ctx() { req }: Context 67 | ) { 68 | const note = await Note.findOne(id, { relations: ["creator"] }); 69 | if (note?.creator.id != req.session.userId) { 70 | return false; 71 | } 72 | await Note.delete({ id }); 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/src/resolvers/upload/avatar.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import multer, { FileFilterCallback } from "multer"; 3 | import aws from "aws-sdk"; 4 | 5 | import { expressIsAuth } from "../../middleware/isAuth"; 6 | import { v4 } from "uuid"; 7 | import multers3 from "multer-s3"; 8 | import { User } from "../../entities/User"; 9 | 10 | const router = Router(); 11 | 12 | const ENDPOINT = new aws.Endpoint(process.env.DO_ENDPOINT); 13 | 14 | const s3 = new aws.S3({ 15 | endpoint: ENDPOINT, 16 | accessKeyId: process.env.DO_ACCESS_KEY_ID, 17 | secretAccessKey: process.env.DO_SECRET_ACCESS_KEY, 18 | }); 19 | 20 | const upload = multer({ 21 | storage: multers3({ 22 | s3, 23 | bucket: "dino-images", 24 | acl: "public-read", 25 | key: async (_: any, _file: any, cb: any) => { 26 | const name = await v4(); 27 | cb(null, "profile/" + name + ".jpg"); // e.g. jh34gh2v4y + .jpg 28 | }, 29 | }), 30 | fileFilter: (_, file: any, callback: FileFilterCallback) => { 31 | console.log("mimetype : ", file.mimetype); 32 | if (file.mimetype.includes("image")) { 33 | callback(null, true); 34 | } else { 35 | callback(new Error("Not an image")); 36 | } 37 | }, 38 | }); 39 | 40 | router.post( 41 | "/avatar", 42 | expressIsAuth, 43 | upload.single("file"), 44 | async (req, res) => { 45 | await User.update( 46 | { id: req.body.userId }, 47 | { 48 | imgUrl: (req.file as any).location, 49 | } 50 | ); 51 | console.log(req.file); 52 | return res.json({ success: true }); 53 | } 54 | ); 55 | 56 | export default router; 57 | -------------------------------------------------------------------------------- /server/src/resolvers/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Mutation, 4 | Arg, 5 | Field, 6 | Ctx, 7 | ObjectType, 8 | Query, 9 | UseMiddleware, 10 | } from "type-graphql"; 11 | import { Context } from "../types"; 12 | import { User } from "../entities/User"; 13 | import argon2 from "argon2"; 14 | import { COOKIE_NAME, FORGET_PASSWORD_PREFIX } from "../constants"; 15 | import { UserInput } from "../schemas/UserInput"; 16 | import { validateRegister } from "../utils/validateRegister"; 17 | import { sendEmail } from "../utils/sendEmail"; 18 | import { v4 } from "uuid"; 19 | import { getConnection } from "typeorm"; 20 | import { isAuth } from "../middleware/isAuth"; 21 | 22 | @ObjectType() 23 | export class FieldError { 24 | @Field() 25 | field: string; 26 | @Field() 27 | message: string; 28 | } 29 | 30 | @ObjectType() 31 | class UserResponse { 32 | @Field(() => [FieldError], { nullable: true }) 33 | errors?: FieldError[]; 34 | 35 | @Field(() => User, { nullable: true }) 36 | user?: User; 37 | } 38 | 39 | @Resolver(User) 40 | export class UserResolver { 41 | @Mutation(() => UserResponse) 42 | async changePassword( 43 | @Arg("token") token: string, 44 | @Arg("newPassword") newPassword: string, 45 | @Ctx() { redis, req }: Context 46 | ): Promise { 47 | if (newPassword.length <= 2) { 48 | return { 49 | errors: [ 50 | { 51 | field: "newPassword", 52 | message: "length must be greater than 2", 53 | }, 54 | ], 55 | }; 56 | } 57 | 58 | const key = FORGET_PASSWORD_PREFIX + token; 59 | const userId = await redis.get(key); 60 | if (!userId) { 61 | return { 62 | errors: [ 63 | { 64 | field: "token", 65 | message: "token expired", 66 | }, 67 | ], 68 | }; 69 | } 70 | 71 | const userIdNum = parseInt(userId); 72 | const user = await User.findOne(userIdNum, { 73 | relations: ["notes", "folders"], 74 | }); 75 | 76 | if (!user) { 77 | return { 78 | errors: [ 79 | { 80 | field: "token", 81 | message: "user no longer exists", 82 | }, 83 | ], 84 | }; 85 | } 86 | 87 | await User.update( 88 | { id: userIdNum }, 89 | { 90 | password: await argon2.hash(newPassword), 91 | } 92 | ); 93 | 94 | await redis.del(key); 95 | 96 | req.session.userId = user.id; 97 | 98 | return { user }; 99 | } 100 | 101 | @Mutation(() => Boolean) 102 | async forgotPassword( 103 | @Arg("email") email: string, 104 | @Ctx() { redis }: Context 105 | ) { 106 | const user = await User.findOne({ where: { email } }); 107 | if (!user) { 108 | return false; 109 | } 110 | 111 | const token = v4(); 112 | 113 | await redis.set( 114 | FORGET_PASSWORD_PREFIX + token, 115 | user.id, 116 | "ex", 117 | 1000 * 60 * 60 * 24 * 3 118 | ); // 3 days 119 | 120 | await sendEmail( 121 | email, 122 | `reset password` 123 | ); 124 | 125 | return true; 126 | } 127 | 128 | @Query(() => User, { nullable: true }) 129 | me(@Ctx() { req }: Context) { 130 | // you are not logged in 131 | if (!req.session.userId) { 132 | return null; 133 | } 134 | 135 | return User.findOne(req.session.userId, { 136 | relations: ["notes", "folders"], 137 | }); 138 | } 139 | 140 | @Mutation(() => UserResponse) 141 | async register( 142 | @Arg("options") options: UserInput, 143 | @Ctx() { req }: Context 144 | ): Promise { 145 | const errors = validateRegister(options); 146 | if (errors) { 147 | return { errors }; 148 | } 149 | 150 | const hashedPassword = await argon2.hash(options.password); 151 | let user; 152 | try { 153 | const result = await getConnection() 154 | .createQueryBuilder() 155 | .insert() 156 | .into(User) 157 | .values({ 158 | name: options.name, 159 | email: options.email, 160 | password: hashedPassword, 161 | }) 162 | .returning("*") 163 | .execute(); 164 | user = result.raw[0]; 165 | } catch (err) { 166 | // duplicate username error 167 | if (err.code === "23505") { 168 | return { 169 | errors: [ 170 | { 171 | field: "email", 172 | message: "email already taken", 173 | }, 174 | ], 175 | }; 176 | } 177 | } 178 | 179 | req.session.userId = user.id; 180 | const us = await User.findOne(user.id, { 181 | relations: ["notes", "folders"], 182 | }); 183 | 184 | return { user: us }; 185 | } 186 | 187 | @Mutation(() => UserResponse) 188 | async login( 189 | @Arg("email") email: string, 190 | @Arg("password") password: string, 191 | @Ctx() { req }: Context 192 | ): Promise { 193 | const user = await User.findOne({ 194 | where: { email }, 195 | relations: ["notes", "folders"], 196 | }); 197 | if (!user) { 198 | return { 199 | errors: [ 200 | { 201 | field: "email", 202 | message: "that account doesn't exist", 203 | }, 204 | ], 205 | }; 206 | } 207 | const valid = await argon2.verify(user.password, password); 208 | if (!valid) { 209 | return { 210 | errors: [ 211 | { 212 | field: "password", 213 | message: "incorrect password", 214 | }, 215 | ], 216 | }; 217 | } 218 | 219 | req.session.userId = user.id; 220 | 221 | return { 222 | user, 223 | }; 224 | } 225 | 226 | @Mutation(() => Boolean) 227 | logout(@Ctx() { req, res }: Context) { 228 | return new Promise((resolve) => 229 | req.session.destroy((err) => { 230 | res.clearCookie(COOKIE_NAME); 231 | if (err) { 232 | console.log(err); 233 | resolve(false); 234 | return; 235 | } 236 | 237 | resolve(true); 238 | }) 239 | ); 240 | } 241 | 242 | @UseMiddleware(isAuth) 243 | @Mutation(() => Boolean) 244 | async updateName(@Arg("name") name: string, @Ctx() { req }: Context) { 245 | await User.update( 246 | { id: req.session.userId }, 247 | { 248 | name, 249 | } 250 | ); 251 | return true; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /server/src/schemas/UserInput.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "type-graphql"; 2 | 3 | @InputType() 4 | export class UserInput { 5 | @Field() 6 | email: string; 7 | @Field() 8 | name: string; 9 | @Field() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /server/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import session from "express-session"; 3 | import { Redis } from "ioredis"; 4 | 5 | declare module "express-session" { 6 | interface SessionData { 7 | userId: any; 8 | } 9 | } 10 | 11 | export type Context = { 12 | req: Request & { session: session.Session }; 13 | redis: Redis; 14 | res: Response; 15 | }; 16 | -------------------------------------------------------------------------------- /server/src/utils/chooseRandomFolderColor.ts: -------------------------------------------------------------------------------- 1 | export const chooseRandomFolderColor = () => { 2 | const colors = [ 3 | "#1e76da", 4 | "#21e6c1", 5 | "#28c7fa", 6 | "#f9ff21", 7 | "#ff1f5a", 8 | "#0da574", 9 | "#e53e3e", 10 | ]; 11 | return colors[Math.floor(Math.random() * colors.length)]; 12 | }; 13 | -------------------------------------------------------------------------------- /server/src/utils/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | 3 | // async..await is not allowed in global scope, must use a wrapper 4 | export async function sendEmail(to: string, html: string) { 5 | let transporter = nodemailer.createTransport({ 6 | host: "smtp.ethereal.email", 7 | port: 587, 8 | secure: false, 9 | auth: { 10 | user: "mds43vi6nviwucqv@ethereal.email", 11 | pass: "xJsQzVAuFYKqx5xUR9", 12 | }, 13 | }); 14 | 15 | // send mail with defined transport object 16 | let info = await transporter.sendMail({ 17 | from: '"Fred Foo 👻" ', 18 | to: to, 19 | subject: "Change password", 20 | html, 21 | }); 22 | 23 | console.log("Message sent: %s", info.messageId); 24 | console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info)); 25 | } 26 | -------------------------------------------------------------------------------- /server/src/utils/validateRegister.ts: -------------------------------------------------------------------------------- 1 | import { UserInput } from "../schemas/UserInput"; 2 | 3 | export const validateRegister = (options: UserInput) => { 4 | if (!options.email.includes("@")) { 5 | return [ 6 | { 7 | field: "email", 8 | message: "invalid email", 9 | }, 10 | ]; 11 | } 12 | 13 | if (options.password.length <= 2) { 14 | return [ 15 | { 16 | field: "password", 17 | message: "length must be greater than 2", 18 | }, 19 | ]; 20 | } 21 | 22 | return null; 23 | }; 24 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], 6 | "skipLibCheck": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "moduleResolution": "node", 10 | "removeComments": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true, 21 | "emitDecoratorMetadata": true, 22 | "experimentalDecorators": true, 23 | "resolveJsonModule": true, 24 | "baseUrl": "." 25 | }, 26 | "exclude": ["node_modules"], 27 | "include": ["./src/**/*.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /web/.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /web/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:4000/graphql" 3 | documents: "src/graphql/**/*.graphql" 4 | generates: 5 | src/generated/graphql.tsx: 6 | plugins: 7 | - "typescript" 8 | - "typescript-operations" 9 | - "typescript-react-apollo" 10 | -------------------------------------------------------------------------------- /web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "gen": "graphql-codegen --config codegen.yml" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.4.17", 13 | "@headlessui/react": "^1.4.2", 14 | "axios": "^0.24.0", 15 | "deep-equal": "^2.0.5", 16 | "formik": "^2.2.9", 17 | "graphql": "^16.0.1", 18 | "is-hotkey": "^0.2.0", 19 | "is-url": "^1.2.4", 20 | "next": "12.0.4", 21 | "react": "17.0.2", 22 | "react-contenteditable": "^3.3.6", 23 | "react-dom": "17.0.2", 24 | "react-icons": "^4.3.1", 25 | "react-responsive-modal": "^6.1.0", 26 | "slate": "^0.70.0", 27 | "slate-history": "^0.66.0", 28 | "slate-instant-replace": "^0.1.16", 29 | "slate-react": "^0.70.2" 30 | }, 31 | "devDependencies": { 32 | "@graphql-codegen/cli": "2.3.0", 33 | "@graphql-codegen/typescript": "2.4.1", 34 | "@graphql-codegen/typescript-operations": "2.2.1", 35 | "@graphql-codegen/typescript-react-apollo": "3.2.2", 36 | "@types/deep-equal": "^1.0.1", 37 | "@types/is-url": "^1.2.30", 38 | "@types/node": "16.11.9", 39 | "@types/react": "17.0.35", 40 | "@types/slate": "^0.47.9", 41 | "@types/slate-react": "^0.50.1", 42 | "autoprefixer": "^10.4.0", 43 | "eslint": "7", 44 | "eslint-config-next": "12.0.4", 45 | "postcss": "^8.3.11", 46 | "tailwindcss": "^2.2.19", 47 | "typescript": "4.5.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web/src/components/shared/Meta.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface MetaProps { 4 | title: string; 5 | owner?: string; 6 | description?: string; 7 | } 8 | 9 | export const Meta: React.FC = ({ title, owner, description }) => { 10 | return ( 11 | <> 12 | 13 | {owner ? : ""} 14 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /web/src/components/shared/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { Fragment, useState } from "react"; 3 | import { 4 | useCreateNoteMutation, 5 | useLogoutMutation, 6 | useMeQuery, 7 | } from "../../generated/graphql"; 8 | import { BiDotsVerticalRounded, BiPlus, BiCog, BiSearch } from "react-icons/bi"; 9 | import { NoteCard } from "../ui/cards/Note"; 10 | import { FolderCard } from "../ui/cards/Folder"; 11 | import { FolderModal } from "../ui/modals/Folder"; 12 | import { useRouter } from "next/router"; 13 | import { useApolloClient } from "@apollo/client"; 14 | import { Menu, Transition } from "@headlessui/react"; 15 | import { SettingsModal } from "../ui/modals/Settings"; 16 | import { QuickFindModal } from "../ui/modals/QuickFind"; 17 | import { Spinner } from "./Spinner"; 18 | 19 | interface SidebarProps {} 20 | 21 | const Sidebar: React.FC = ({}) => { 22 | const { data, loading } = useMeQuery(); 23 | const [open, setOpen] = useState(false); 24 | const [settingsOpen, setSettingsOpen] = useState(false); 25 | const [findOpen, setFindOpen] = useState(false); 26 | const [createNote] = useCreateNoteMutation(); 27 | const router = useRouter(); 28 | const client = useApolloClient(); 29 | const [logout] = useLogoutMutation(); 30 | 31 | const newNote = async () => { 32 | const note = await createNote({ 33 | variables: { 34 | title: "😃 New Note", 35 | }, 36 | }); 37 | const id = note.data?.createNote.id; 38 | router.push(`/app/n/${id}`); 39 | await client.resetStore(); 40 | }; 41 | 42 | const logUserOut = async () => { 43 | await logout(); 44 | router.push("/"); 45 | await client.resetStore(); 46 | }; 47 | 48 | return ( 49 | <> 50 | {data && !loading ? ( 51 |
52 | 53 |
54 | 55 | {data.me?.name} 60 |

61 | {data.me?.name} 62 |

63 | 64 |
65 |
66 | 75 | 76 |
77 |
78 |

79 | {data.me?.email} 80 |

81 |
82 |
83 | {data.me?.name} 88 |
89 |

90 | {data.me?.name} 91 |

92 | 93 |

94 | Standard Plan 95 |

96 |
97 |
98 |

102 | Log out 103 |

104 |
105 |
106 |
107 |
108 |
setFindOpen(true)} 110 | className="flex items-center px-2 py-1.5 cursor-pointer hover:bg-gray-200 dark:hover:bg-black-600" 111 | > 112 | 113 |

114 | Quick Find 115 |

116 |
117 |
setSettingsOpen(true)} 119 | className="flex items-center px-2 py-1.5 cursor-pointer hover:bg-gray-200 dark:hover:bg-black-600" 120 | > 121 | 122 |

123 | Settings 124 |

125 |
126 | 171 |
172 | ) : ( 173 |
174 | 175 |
176 | )} 177 | 178 | 179 | 180 | 181 | ); 182 | }; 183 | 184 | export default Sidebar; 185 | -------------------------------------------------------------------------------- /web/src/components/shared/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type SpinnerProps = React.DetailedHTMLProps< 4 | React.HTMLAttributes, 5 | HTMLDivElement 6 | > & {}; 7 | 8 | export const Spinner: React.FC = ({ ...props }) => { 9 | return ( 10 |
11 | Loading... 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /web/src/components/shared/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Sidebar from "./Sidebar"; 3 | 4 | interface WrapperProps {} 5 | 6 | export const Wrapper: React.FC = ({ children }) => { 7 | return ( 8 |
9 | 10 |
11 |
{children}
12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/components/ui/ColoredCircle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ColoredCircleProps { 4 | color: string; 5 | } 6 | 7 | export const ColoredCircle: React.FC = ({ color }) => { 8 | return ( 9 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /web/src/components/ui/InputField.tsx: -------------------------------------------------------------------------------- 1 | import { useField } from "formik"; 2 | import React, { InputHTMLAttributes } from "react"; 3 | 4 | type InputFieldProps = InputHTMLAttributes & { 5 | name: string; 6 | label: string; 7 | }; 8 | 9 | export const InputField: React.FC = ({ label, ...props }) => { 10 | const [field, { error }] = useField(props as any); 11 | return ( 12 |
13 | 16 | 27 | {error ? ( 28 | 29 | {error} 30 | 31 | ) : null} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /web/src/components/ui/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useEffect, useState } from "react"; 2 | import { 3 | BaseEditor, 4 | Editor as StaticEditor, 5 | Range, 6 | Transforms, 7 | Element as SlateElement, 8 | Descendant, 9 | } from "slate"; 10 | import { ReactEditor, useSlateStatic } from "slate-react"; 11 | import { Icon } from "./editor/core/navbar/Icon"; 12 | import { ParaStyleDropDown } from "./editor/core/navbar/ParaStyleDropdown"; 13 | import { MdOutlineDelete } from "react-icons/md"; 14 | import { BiImageAlt, BiLink } from "react-icons/bi"; 15 | import { DeleteNoteModal } from "./modals/DeleteNote"; 16 | import { useRouter } from "next/router"; 17 | import { 18 | GetNoteQuery, 19 | useAddNoteToFolderMutation, 20 | useDeleteNoteFromFolderMutation, 21 | useMeQuery, 22 | } from "../../generated/graphql"; 23 | import { findNoteFolder } from "../../utils/findNoteFolder"; 24 | import { Listbox, Transition } from "@headlessui/react"; 25 | import { findFolderId } from "../../utils/FindFolderId"; 26 | import { useApolloClient } from "@apollo/client"; 27 | import { insertImage } from "./editor/core/renderElement"; 28 | import { AddImageModal } from "./modals/AddImage"; 29 | import { timeSince, timeSinceShort } from "../../utils/timeSince"; 30 | 31 | interface NavbarProps { 32 | saving: boolean; 33 | id: number; 34 | note: GetNoteQuery["getNote"]; 35 | } 36 | type SlateEditor = BaseEditor & ReactEditor; 37 | 38 | function getActiveStyles(editor: SlateEditor) { 39 | return new Set(Object.keys(StaticEditor.marks(editor) ?? {})); 40 | } 41 | 42 | export function isLinkNodeAtSelection(editor: SlateEditor, selection: any) { 43 | if (selection == null) { 44 | return false; 45 | } 46 | 47 | return ( 48 | StaticEditor.above(editor, { 49 | at: selection, 50 | match: (n: any) => n.type === "link", 51 | }) != null 52 | ); 53 | } 54 | 55 | export function toggleStyle(editor: SlateEditor, style: any) { 56 | const activeStyles = getActiveStyles(editor); 57 | if (activeStyles.has(style)) { 58 | StaticEditor.removeMark(editor, style); 59 | } else { 60 | StaticEditor.addMark(editor, style, true); 61 | } 62 | } 63 | 64 | const CHARACTER_STYLES = ["bold", "italic", "underline", "code", "highlighted"]; 65 | 66 | function getTextBlockStyle(editor: SlateEditor) { 67 | const selection = editor.selection; 68 | if (selection == null) { 69 | return null; 70 | } 71 | // gives the forward-direction points in case the selection was 72 | // was backwards. 73 | const [start, end] = Range.edges(selection); 74 | 75 | //path[0] gives us the index of the top-level block. 76 | let startTopLevelBlockIndex = start.path[0]; 77 | const endTopLevelBlockIndex = end.path[0]; 78 | 79 | let blockType = null; 80 | while (startTopLevelBlockIndex <= endTopLevelBlockIndex) { 81 | const [node, _] = StaticEditor.node(editor, [startTopLevelBlockIndex]); 82 | if (blockType == null) { 83 | blockType = (node as any).type; 84 | } else if (blockType !== (node as any).type) { 85 | return "multiple"; 86 | } 87 | startTopLevelBlockIndex++; 88 | } 89 | 90 | return blockType; 91 | } 92 | 93 | function toggleBlockType(editor: SlateEditor, blockType: any) { 94 | const currentBlockType = getTextBlockStyle(editor); 95 | const changeTo = currentBlockType === blockType ? "paragraph" : blockType; 96 | Transforms.setNodes( 97 | editor, 98 | { type: changeTo }, 99 | { 100 | at: editor.selection as any, 101 | match: (n) => StaticEditor.isBlock(editor, n), 102 | } 103 | ); 104 | } 105 | 106 | export const Navbar: React.FC = ({ saving, id, note }) => { 107 | const editor = useSlateStatic(); 108 | const onBlockTypeChange = useCallback( 109 | (targetType) => { 110 | if (targetType === "multiple") { 111 | return; 112 | } 113 | toggleBlockType(editor, targetType); 114 | }, 115 | [editor] 116 | ); 117 | const blockType = getTextBlockStyle(editor); 118 | const [open, setOpen] = useState(false); 119 | const [imageOpen, setImageOpen] = useState(false); 120 | const router = useRouter(); 121 | const { data, loading } = useMeQuery(); 122 | const [selected, setSelected] = useState( 123 | findNoteFolder(id, data && !loading && data.me?.folders) 124 | ); 125 | const [deleteNoteFromFolderMutation] = useDeleteNoteFromFolderMutation(); 126 | const [addNoteToFolderMutation] = useAddNoteToFolderMutation(); 127 | const client = useApolloClient(); 128 | 129 | useEffect(() => { 130 | const currentFolder = findNoteFolder(id, data?.me?.folders); 131 | const currentFolderId = findFolderId( 132 | currentFolder, 133 | (data as any).me?.folders as any 134 | ); 135 | console.log(currentFolderId); 136 | if (currentFolder == selected) { 137 | return; 138 | } 139 | (async () => { 140 | await deleteNoteFromFolderMutation({ 141 | variables: { 142 | folderId: currentFolderId, 143 | noteId: id, 144 | }, 145 | }); 146 | })(); 147 | 148 | if (selected == "No Folder") { 149 | (async () => { 150 | await client.resetStore(); 151 | })(); 152 | return; 153 | } 154 | 155 | const newFolderId = findFolderId( 156 | selected, 157 | (data as any).me?.folders as any 158 | ); 159 | (async () => { 160 | await addNoteToFolderMutation({ 161 | variables: { 162 | folderId: newFolderId, 163 | noteId: id, 164 | }, 165 | }); 166 | await client.resetStore(); 167 | })(); 168 | }, [selected]); 169 | 170 | return ( 171 |
172 | 176 | {CHARACTER_STYLES.map((style, i) => ( 177 | 187 | ))} 188 | setImageOpen(true)} 193 | /> 194 | {saving ? ( 195 |

196 | Saving... 197 |

198 | ) : ( 199 |

200 | Saved{" "} 201 | 202 | {timeSinceShort(note.updatedAt)} ago 203 | 204 |

205 | )} 206 |
207 | {data && !loading && ( 208 | <> 209 |
210 | 211 |
212 | 213 | 214 | {selected} 215 | 216 | 217 | 223 | 224 | {data.me?.folders.map((folder) => ( 225 | 230 | 231 | {folder.name} 232 | 233 | 234 | ))} 235 | 239 | 240 | No Folder 241 | 242 | 243 | 244 | 245 |
246 |
247 |
248 | setOpen(true)} 250 | className="p-1 ml-2 rounded-sm cursor-pointer w-7 h-7 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" 251 | /> 252 | 253 | )} 254 |
255 | 264 | 269 |
270 | ); 271 | }; 272 | -------------------------------------------------------------------------------- /web/src/components/ui/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface WelcomeProps {} 4 | 5 | export const Welcome: React.FC = ({}) => { 6 | return ( 7 |
8 |

9 | Welcome to dino.app 10 |

11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /web/src/components/ui/cards/Folder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from "react"; 2 | import { 3 | BiCaretRight, 4 | BiCaretDown, 5 | BiDotsVerticalRounded, 6 | } from "react-icons/bi"; 7 | import { 8 | useGetNoteQuery, 9 | useDeleteFolderMutation, 10 | } from "../../../generated/graphql"; 11 | import NextLink from "next/link"; 12 | import { MdOutlineDelete } from "react-icons/md"; 13 | import { Menu, Transition } from "@headlessui/react"; 14 | import { useApolloClient } from "@apollo/client"; 15 | import { ColoredCircle } from "../ColoredCircle"; 16 | 17 | interface FolderCardProps { 18 | name: string; 19 | notes: string[]; 20 | id: number; 21 | color: string; 22 | } 23 | 24 | export const FolderCard: React.FC = ({ 25 | name, 26 | notes, 27 | id, 28 | color, 29 | }) => { 30 | const [showContents, setShowContents] = useState(false); 31 | const [deleteFolderMutation] = useDeleteFolderMutation(); 32 | const client = useApolloClient(); 33 | 34 | const deleteFolder = async () => { 35 | await deleteFolderMutation({ 36 | variables: { 37 | id, 38 | }, 39 | }); 40 | await client.resetStore(); 41 | }; 42 | 43 | return ( 44 |
45 |
setShowContents(!showContents)} 47 | className="group flex cursor-pointer items-center pl-1 px-2 py-0.5 hover:bg-gray-200 dark:hover:bg-black-600" 48 | > 49 |
50 | {showContents ? ( 51 | 52 | ) : ( 53 | 54 | )} 55 | 56 |
57 |

58 | {name} 59 |

60 | 61 |
62 | 65 | 66 | 67 |
68 | 77 | 78 |
79 |
83 | 84 |

85 | Delete 86 |

87 |
88 |
89 |
90 |
91 |
92 |
93 | {showContents && ( 94 |
95 | {notes.map((note, i) => ( 96 | 97 | ))} 98 |
99 | )} 100 |
101 | ); 102 | }; 103 | 104 | interface FolderNoteCardProps { 105 | id: string; 106 | } 107 | 108 | export const FolderNoteCard: React.FC = ({ id }) => { 109 | const { data } = useGetNoteQuery({ 110 | variables: { 111 | id: parseInt(id), 112 | }, 113 | }); 114 | return ( 115 | 116 | 117 |
118 |

119 | {data?.getNote.title} 120 |

121 |
122 |
123 |
124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /web/src/components/ui/cards/Note.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "next/link"; 3 | import { useMeQuery } from "../../../generated/graphql"; 4 | import { ColoredCircle } from "../ColoredCircle"; 5 | import { findNoteFolder } from "../../../utils/findNoteFolder"; 6 | 7 | interface NoteCardProps { 8 | name: string; 9 | status: string; 10 | id: number; 11 | } 12 | 13 | export const NoteCard: React.FC = ({ name, status, id }) => { 14 | const { data, loading } = useMeQuery(); 15 | return ( 16 | 17 | 18 |
19 |
20 | 24 | fold.name == 25 | findNoteFolder(id, data?.me?.folders) 26 | )[0]?.color || "#28c077" 27 | } 28 | /> 29 |
30 |

31 | {name} 32 |

33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /web/src/components/ui/editor/core/navbar/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BiBold, 3 | BiItalic, 4 | BiCode, 5 | BiUnderline, 6 | BiHighlight, 7 | BiLink, 8 | } from "react-icons/bi"; 9 | import { 10 | BaseEditor, 11 | Descendant, 12 | Range, 13 | Element as SlateElement, 14 | Transforms, 15 | Editor as StaticEditor, 16 | } from "slate"; 17 | import { ReactEditor } from "slate-react"; 18 | import { toggleStyle } from "../../../Navbar"; 19 | 20 | interface IconProps { 21 | style: string; 22 | isActive: boolean; 23 | editor: SlateEditor; 24 | } 25 | type SlateEditor = BaseEditor & ReactEditor; 26 | 27 | type LinkElement = { type: "link"; url: string; children: Descendant[] }; 28 | 29 | export const Icon: React.FC = ({ style, isActive, editor }) => { 30 | const normalClassName = 31 | "p-1 w-7 h-7 text-gray-800 dark:text-gray-200 cursor-pointer border border-white dark:border-black-navbar mx-1"; 32 | 33 | const activeClassName = 34 | "p-1 w-7 h-7 text-gray-800 cursor-pointer dark:bg-black-pantone bg-gray-100 rounded-sm mx-1 border border-gray-300 dark:border-gray-700 dark:text-gray-200"; 35 | 36 | const toggleMark = (e: React.MouseEvent) => { 37 | e.preventDefault(); 38 | toggleStyle(editor, style); 39 | }; 40 | 41 | const normalIconMap: Record = { 42 | bold: , 43 | italic: , 44 | underline: ( 45 | 46 | ), 47 | code: , 48 | highlighted: ( 49 | 50 | ), 51 | }; 52 | 53 | const activeIconMap: Record = { 54 | bold: , 55 | italic: , 56 | underline: ( 57 | 58 | ), 59 | code: , 60 | highlighted: ( 61 | 62 | ), 63 | }; 64 | 65 | return ( 66 | <> 67 | {isActive ? ( 68 | <>{activeIconMap[style]} 69 | ) : ( 70 | <>{normalIconMap[style]} 71 | )} 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /web/src/components/ui/editor/core/navbar/ParaStyleDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { Listbox, Transition } from "@headlessui/react"; 3 | 4 | const PARAGRAPH_STYLES = ["H1", "H2", "H3", "H4", "Paragraph", "Multiple"]; 5 | 6 | export const ParaStyleDropDown: React.FC<{ onChange: any; initialValue: any }> = 7 | ({ onChange, initialValue }) => { 8 | return ( 9 | <> 10 | {initialValue != null && ( 11 |
12 | 13 |
14 | 15 | 16 | {initialValue} 17 | 18 | 19 | 25 | 26 | {PARAGRAPH_STYLES.map((style, i) => ( 27 | 32 | 33 | {style} 34 | 35 | 36 | ))} 37 | 38 | 39 |
40 |
41 |
42 | )} 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /web/src/components/ui/editor/core/renderElement.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useSelected, 3 | useFocused, 4 | useSlateStatic, 5 | DefaultElement, 6 | } from "slate-react"; 7 | import { Editor, Transforms, Path, Node, Element } from "slate"; 8 | import { ReactEditor } from "slate-react"; 9 | import { SlateEditor } from "../editor"; 10 | import router from "next/router"; 11 | 12 | export const renderElement = (props: any) => { 13 | const { element, children, attributes } = props; 14 | switch (element.type) { 15 | case "paragraph": 16 | return

{children}

; 17 | case "H1": 18 | return ( 19 |

23 | {children} 24 |

25 | ); 26 | case "H2": 27 | return ( 28 |

32 | {children} 33 |

34 | ); 35 | case "H3": 36 | return ( 37 |

41 | {children} 42 |

43 | ); 44 | case "H4": 45 | return ( 46 |

50 | {children} 51 |

52 | ); 53 | case "code": 54 | return {children}; 55 | case "image": 56 | return ; 57 | default: 58 | return ; 59 | } 60 | }; 61 | 62 | const Image = ({ attributes, element, children }: any) => { 63 | const selected = useSelected(); 64 | const focused = useFocused(); 65 | 66 | return ( 67 |
75 |
76 | {element.alt} 81 |
82 | {children} 83 |
84 | ); 85 | }; 86 | 87 | export const insertImage = (editor: SlateEditor, url: any) => { 88 | if (!url) return; 89 | 90 | const { selection } = editor; 91 | const image = { 92 | type: "image", 93 | alt: "", 94 | url, 95 | children: [{ text: "" }], 96 | }; 97 | console.log(image); 98 | 99 | ReactEditor.focus(editor); 100 | 101 | if (!!selection) { 102 | const [parentNode, parentPath] = Editor.parent( 103 | editor, 104 | selection.focus?.path 105 | ); 106 | 107 | if ( 108 | // @ts-ignore 109 | editor.isVoid(parentNode) || 110 | (Node as any).string(parentNode).length 111 | ) { 112 | // Insert the new image node after the void node or a node with content 113 | Transforms.insertNodes(editor, image, { 114 | at: Path.next(parentPath), 115 | select: true, 116 | }); 117 | } else { 118 | // If the node is empty, replace it instead 119 | Transforms.removeNodes(editor, { at: parentPath }); 120 | Transforms.insertNodes(editor, image, { 121 | at: parentPath, 122 | select: true, 123 | }); 124 | } 125 | } else { 126 | // Insert the new image node at the bottom of the Editor when selection 127 | // is falsey 128 | Transforms.insertNodes(editor, image, { select: true }); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /web/src/components/ui/editor/core/renderLeaf.tsx: -------------------------------------------------------------------------------- 1 | import { useSelected } from "slate-react"; 2 | 3 | export const renderLeaf = ({ attributes, children, leaf }: any) => { 4 | let el = <>{children}; 5 | 6 | if (leaf.bold) { 7 | el = {el}; 8 | } 9 | 10 | if (leaf.code) { 11 | el = {el}; 12 | } 13 | 14 | if (leaf.italic) { 15 | el = {el}; 16 | } 17 | 18 | if (leaf.underline) { 19 | el = {el}; 20 | } 21 | 22 | if (leaf.highlighted) { 23 | el = ( 24 | 25 | {el} 26 | 27 | ); 28 | } 29 | 30 | return {el}; 31 | }; 32 | -------------------------------------------------------------------------------- /web/src/components/ui/editor/editor.tsx: -------------------------------------------------------------------------------- 1 | import { useApolloClient } from "@apollo/client"; 2 | import areEqual from "deep-equal"; 3 | import isHotkey from "is-hotkey"; 4 | import isUrl from "is-url"; 5 | import React, { 6 | createRef, 7 | useCallback, 8 | useEffect, 9 | useMemo, 10 | useRef, 11 | useState, 12 | } from "react"; 13 | import { BaseEditor, createEditor, Descendant } from "slate"; 14 | import { Editable, ReactEditor, Slate, withReact } from "slate-react"; 15 | import { 16 | GetNoteQuery, 17 | UpdateNoteTitleDocument, 18 | useGetNoteQuery, 19 | useUpdateNoteMutation, 20 | useUpdateNoteTitleMutation, 21 | } from "../../../generated/graphql"; 22 | import { Navbar, toggleStyle } from "../Navbar"; 23 | import { renderElement } from "./core/renderElement"; 24 | import { renderLeaf } from "./core/renderLeaf"; 25 | import ContentEditable from "react-contenteditable"; 26 | import { withHistory } from "slate-history"; 27 | 28 | interface EditorProps { 29 | note: GetNoteQuery["getNote"]; 30 | } 31 | 32 | type CustomElement = { 33 | type: string; 34 | url?: string; 35 | children: CustomText[]; 36 | }; 37 | type CustomText = { text: string }; 38 | declare module "slate" { 39 | interface CustomTypes { 40 | Editor: BaseEditor & ReactEditor; 41 | Element: CustomElement; 42 | Text: CustomText; 43 | } 44 | } 45 | export type SlateEditor = BaseEditor & ReactEditor; 46 | 47 | const useEditorConfig = (editor: SlateEditor) => { 48 | const onKeyDown = useCallback( 49 | (event) => KeyBindings.onKeyDown(editor, event), 50 | [editor] 51 | ); 52 | const { isVoid, isInline } = editor; 53 | editor.isVoid = (element) => { 54 | return ["image"].includes(element.type) || isVoid(element); 55 | editor.isInline = (element) => 56 | ["link"].includes(element.type) || isInline(element); 57 | }; 58 | return { renderElement, renderLeaf, onKeyDown }; 59 | }; 60 | 61 | const KeyBindings = { 62 | onKeyDown: (editor: any, event: any) => { 63 | if (isHotkey("mod+b", event)) { 64 | toggleStyle(editor, "bold"); 65 | return; 66 | } 67 | if (isHotkey("mod+i", event)) { 68 | toggleStyle(editor, "italic"); 69 | return; 70 | } 71 | if (isHotkey("mod+`", event)) { 72 | toggleStyle(editor, "code"); 73 | return; 74 | } 75 | if (isHotkey("mod+u", event)) { 76 | toggleStyle(editor, "underline"); 77 | return; 78 | } 79 | if (isHotkey("mod+h", event)) { 80 | toggleStyle(editor, "highlighted"); 81 | return; 82 | } 83 | }, 84 | }; 85 | 86 | const useSelection = (editor: SlateEditor) => { 87 | const [selection, setSelection] = useState(editor.selection); 88 | const setSelectionOptimized = useCallback( 89 | (newSelection) => { 90 | // don't update the component state if selection hasn't changed. 91 | if (areEqual(selection, newSelection)) { 92 | return; 93 | } 94 | setSelection(newSelection); 95 | }, 96 | [setSelection, selection] 97 | ); 98 | 99 | return [selection, setSelectionOptimized]; 100 | }; 101 | 102 | export const Editor: React.FC = ({ note }) => { 103 | const editor = useMemo(() => withReact(withHistory(createEditor())), []); 104 | const [value, setValue] = useState( 105 | note.body == "" 106 | ? [ 107 | { 108 | type: "Paragraph", 109 | children: [{ text: "" }], 110 | }, 111 | ] 112 | : (JSON.parse(note.body).value as any) 113 | ); 114 | const { renderElement, onKeyDown } = useEditorConfig(editor); 115 | const [selection, setSelection] = useSelection(editor); 116 | const editorRef = useRef(null); 117 | const onChangeHandler = useCallback( 118 | (document) => { 119 | setValue(document); 120 | // @ts-ignore 121 | setSelection(editor.selection); 122 | }, 123 | [editor.selection, setSelection] 124 | ); 125 | const [updateNoteMutation, { loading }] = useUpdateNoteMutation(); 126 | const [updateNoteTitleMutation, { loading: titleChangeLoading }] = 127 | useUpdateNoteTitleMutation(); 128 | const [title, setTitle] = useState(note.title); 129 | const client = useApolloClient(); 130 | 131 | useEffect(() => { 132 | console.log(value); 133 | const timeout = setTimeout(async () => { 134 | await updateNoteMutation({ 135 | variables: { 136 | id: note.id, 137 | body: JSON.stringify({ value }), 138 | }, 139 | }); 140 | }, 500); 141 | 142 | return () => clearTimeout(timeout); 143 | }, [value]); 144 | 145 | useEffect(() => { 146 | const timeout = setTimeout(async () => { 147 | await updateNoteTitleMutation({ 148 | variables: { 149 | id: note.id, 150 | title: title.replace( 151 | /(?:^(?: )+)|(?:(?: )+$)/g, 152 | "" 153 | ), 154 | }, 155 | }); 156 | await client.resetStore(); 157 | }, 300); 158 | 159 | return () => clearTimeout(timeout); 160 | }, [title]); 161 | 162 | const editableRef = useRef(); 163 | const [editableText, setEditableText] = useState("Edit me."); 164 | return ( 165 |
166 | 167 | 172 |
173 | { 179 | if (e.target.value.trim().length == 0) { 180 | return; 181 | } else { 182 | setTitle(e.target.value); 183 | } 184 | }} 185 | /> 186 |
187 | ( 193 |
194 |

{children}

195 |
196 | )} 197 | onKeyDown={onKeyDown} 198 | /> 199 |
200 |
201 |
202 |
203 | ); 204 | }; 205 | -------------------------------------------------------------------------------- /web/src/components/ui/modals/AddImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { SlateEditor } from "../editor/editor"; 4 | import { insertImage } from "../editor/core/renderElement"; 5 | 6 | interface AddImageModalProps { 7 | open: boolean; 8 | setOpen: any; 9 | editor: SlateEditor; 10 | } 11 | 12 | export const AddImageModal: React.FC = ({ 13 | open, 14 | setOpen, 15 | editor, 16 | }) => { 17 | const [imageUrl, setImageUrl] = useState(""); 18 | 19 | const handleInsertImage = () => { 20 | insertImage(editor, imageUrl); 21 | setOpen(false); 22 | setImageUrl(""); 23 | }; 24 | 25 | return ( 26 | 27 | 32 |
33 | 42 | 43 | 44 | 45 | {/* This element is to trick the browser into centering the modal contents. */} 46 | 52 | 61 |
62 |

67 | Image URL 68 |

69 | setImageUrl(e.target.value)} 73 | className="w-full p-1 px-2 mt-2 text-sm bg-gray-100 border border-gray-300 rounded-sm focus:outline-none focus:ring focus:border-blue-100 dark:bg-black-700 dark:border-gray-600 dark:text-gray-200" 74 | /> 75 |
76 | 82 | 88 |
89 |
90 |
91 |
92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /web/src/components/ui/modals/DeleteNote.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { useDeleteNoteMutation } from "../../../generated/graphql"; 4 | import { useRouter } from "next/router"; 5 | import { useApolloClient } from "@apollo/client"; 6 | 7 | interface DeleteNoteModalProps { 8 | open: boolean; 9 | setOpen: any; 10 | id: number; 11 | } 12 | 13 | export const DeleteNoteModal: React.FC = ({ 14 | open, 15 | setOpen, 16 | id, 17 | }) => { 18 | const [deleteNoteMutation] = useDeleteNoteMutation(); 19 | const router = useRouter(); 20 | const client = useApolloClient(); 21 | 22 | const deleteNote = async () => { 23 | await deleteNoteMutation({ 24 | variables: { 25 | id, 26 | }, 27 | }); 28 | router.push(`/app`); 29 | await client.resetStore(); 30 | }; 31 | 32 | return ( 33 | 34 | 39 |
40 | 49 | 50 | 51 | 52 | {/* This element is to trick the browser into centering the modal contents. */} 53 | 59 | 68 |
69 |

70 | Are you sure you want to delete this note ? 71 |

72 |
73 | 79 | 85 |
86 |
87 |
88 |
89 |
90 |
91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /web/src/components/ui/modals/Folder.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { BiRightArrowAlt } from "react-icons/bi"; 4 | import { useCreateFolderMutation } from "../../../generated/graphql"; 5 | import { useApolloClient } from "@apollo/client"; 6 | import { Spinner } from "../../shared/Spinner"; 7 | 8 | interface FolderModalProps { 9 | open: boolean; 10 | setOpen: any; 11 | } 12 | 13 | export const FolderModal: React.FC = ({ open, setOpen }) => { 14 | const [folderName, setFolderName] = useState(""); 15 | const [createFolderMutation, { loading }] = useCreateFolderMutation(); 16 | const client = useApolloClient(); 17 | 18 | const createFolder = async () => { 19 | if (folderName.trim().length == 0) { 20 | return; 21 | } 22 | await createFolderMutation({ 23 | variables: { 24 | name: folderName, 25 | }, 26 | }); 27 | await client.resetStore(); 28 | setFolderName(""); 29 | setOpen(false); 30 | }; 31 | 32 | return ( 33 | 34 | 39 |
40 | 49 | 50 | 51 | 52 | {/* This element is to trick the browser into centering the modal contents. */} 53 | 59 | 68 |
69 |
70 | 74 | setFolderName(e.target.value) 75 | } 76 | value={folderName} 77 | /> 78 | {loading ? ( 79 | 80 | ) : ( 81 | 92 | )} 93 |
94 |
95 |
96 |
97 |
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /web/src/components/ui/modals/QuickFind.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { BiSearch } from "react-icons/bi"; 4 | import { useMeQuery } from "../../../generated/graphql"; 5 | import { findNoteFolder } from "../../../utils/findNoteFolder"; 6 | import { searchNotes } from "../../../utils/searchNotes"; 7 | import NextLink from "next/link"; 8 | 9 | interface QuickFindProps { 10 | open: boolean; 11 | setOpen: any; 12 | } 13 | 14 | export const QuickFindModal: React.FC = ({ open, setOpen }) => { 15 | const { data, loading } = useMeQuery(); 16 | const [query, setQuery] = useState(""); 17 | return ( 18 | 19 | 24 |
25 | 34 | 35 | 36 | 37 | {/* This element is to trick the browser into centering the modal contents. */} 38 | 44 | 53 |
54 |
55 | 56 | setQuery(e.target.value)} 61 | /> 62 |
63 |
64 |

65 | NOTES 66 |

67 |
68 | {query.trim().length == 0 && ( 69 | <> 70 | {data?.me?.notes.slice(0, 7).map((note) => ( 71 | 75 | 76 |
77 |

78 | {note.title} 79 |

80 | {findNoteFolder( 81 | note.id, 82 | data.me?.folders 83 | ) != "No Folder" && ( 84 |

85 | {findNoteFolder( 86 | note.id, 87 | data.me?.folders 88 | )} 89 |

90 | )} 91 |
92 |
93 |
94 | ))} 95 | 96 | )} 97 | {query.trim().length != 0 && ( 98 | <> 99 | {searchNotes( 100 | query, 101 | data?.me?.notes as any 102 | ).map((note) => ( 103 | 107 | 108 |
109 |

110 | {note.title} 111 |

112 | {findNoteFolder( 113 | note.id, 114 | data?.me?.folders 115 | ) != "No Folder" && ( 116 |

117 | {findNoteFolder( 118 | note.id, 119 | data?.me 120 | ?.folders 121 | )} 122 |

123 | )} 124 |
125 |
126 |
127 | ))} 128 | 129 | )} 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /web/src/components/ui/modals/Settings.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import React, { Fragment, useState, createRef, ChangeEvent } from "react"; 3 | import { Dialog, Transition } from "@headlessui/react"; 4 | import { 5 | useForgotPasswordMutation, 6 | useLogoutMutation, 7 | useMeQuery, 8 | useUpdateNameMutation, 9 | } from "../../../generated/graphql"; 10 | import { useApolloClient } from "@apollo/client"; 11 | import { useRouter } from "next/router"; 12 | import { Spinner } from "../../shared/Spinner"; 13 | import Axios from "axios"; 14 | 15 | interface SettingsModalProps { 16 | open: boolean; 17 | setOpen: any; 18 | } 19 | 20 | export const SettingsModal: React.FC = ({ 21 | open, 22 | setOpen, 23 | }) => { 24 | const { data, loading } = useMeQuery(); 25 | const [name, setName] = useState(data && !loading && data?.me?.name); 26 | const [sentEmailLink, setSentEmailLink] = useState(false); 27 | const [forgotPasswordMutation] = useForgotPasswordMutation(); 28 | const client = useApolloClient(); 29 | const router = useRouter(); 30 | const [logout] = useLogoutMutation(); 31 | const [updateNameMutation] = useUpdateNameMutation(); 32 | const fileInputRef = createRef(); 33 | 34 | const forgotPassword = async () => { 35 | await forgotPasswordMutation({ 36 | variables: { 37 | email: data?.me?.email || "", 38 | }, 39 | }); 40 | setSentEmailLink(true); 41 | }; 42 | 43 | const logUserOut = async () => { 44 | await logout(); 45 | router.push("/"); 46 | await client.resetStore(); 47 | }; 48 | 49 | const updateName = async () => { 50 | await updateNameMutation({ 51 | variables: { 52 | name: name || data?.me?.name || "", 53 | }, 54 | }); 55 | await client.resetStore(); 56 | }; 57 | 58 | const uploadImage = async (event: ChangeEvent) => { 59 | const file = (event.target as any).files[0]; 60 | console.log(file); 61 | 62 | const formData = new FormData(); 63 | formData.append("file", file); 64 | formData.append("userId", data?.me?.id as any); 65 | 66 | try { 67 | await Axios.post( 68 | `${process.env.NEXT_PUBLIC_API_URL}/upload/avatar`, 69 | formData, 70 | { 71 | headers: { "Content-Type": "multipart/form-data" }, 72 | withCredentials: true, 73 | } 74 | ); 75 | await client.resetStore(); 76 | } catch (err) { 77 | console.log(err); 78 | } 79 | }; 80 | 81 | return ( 82 | 83 | 88 |
89 | 98 | 99 | 100 | 101 | {/* This element is to trick the browser into centering the modal contents. */} 102 | 108 | 117 |
123 | {data && !loading ? ( 124 | <> 125 |

126 | Account 127 |

128 |

129 | Photo 130 |

131 | {data.me?.name} 136 |
137 | 143 | 153 |
154 |
155 |

156 | Personal Info 157 |

158 |

159 | Email 160 |

161 |

162 | {data.me?.email} 163 |

164 |

165 | Name 166 |

167 | 171 | setName(e.target.value) 172 | } 173 | /> 174 | {data && !loading && name != data.me?.name && ( 175 | <> 176 | 182 | 183 | )} 184 |
185 |

186 | Password 187 |

188 |

189 | Log out after reseting the password, to 190 | use it 191 |

192 |
193 | 199 |
200 | {sentEmailLink && ( 201 |

202 | We{"'"}ve sent an email with a link 203 | to change your password! 204 |

205 | )} 206 |
207 |

208 | Log out of all devices 209 |

210 |

211 | You will be logged out of all other 212 | active sessions besides this one and 213 | will have to log back in. 214 |

215 | 221 |
222 |

223 | Danger zone Info 224 |

225 | 228 | 229 | ) : ( 230 |
231 | 232 |
233 | )} 234 |
235 |
236 |
237 |
238 |
239 | ); 240 | }; 241 | -------------------------------------------------------------------------------- /web/src/graphql/fragments/RegularError.graphql: -------------------------------------------------------------------------------- 1 | fragment RegularError on FieldError { 2 | field 3 | message 4 | } 5 | -------------------------------------------------------------------------------- /web/src/graphql/fragments/RegularFolder.graphql: -------------------------------------------------------------------------------- 1 | fragment RegularFolder on Folder { 2 | id 3 | name 4 | color 5 | noteIds 6 | creatorId 7 | createdAt 8 | updatedAt 9 | } 10 | -------------------------------------------------------------------------------- /web/src/graphql/fragments/RegularNote.graphql: -------------------------------------------------------------------------------- 1 | fragment RegularNote on Note { 2 | id 3 | title 4 | body 5 | status 6 | creatorId 7 | createdAt 8 | updatedAt 9 | } 10 | -------------------------------------------------------------------------------- /web/src/graphql/fragments/RegularUser.graphql: -------------------------------------------------------------------------------- 1 | fragment RegularUser on User { 2 | id 3 | name 4 | theme 5 | imgUrl 6 | email 7 | createdAt 8 | updatedAt 9 | notes { 10 | ...RegularNote 11 | } 12 | folders { 13 | ...RegularFolder 14 | } 15 | __typename 16 | } 17 | -------------------------------------------------------------------------------- /web/src/graphql/fragments/RegularUserResponse.graphql: -------------------------------------------------------------------------------- 1 | fragment RegularUserResponse on UserResponse { 2 | errors { 3 | ...RegularError 4 | } 5 | user { 6 | ...RegularUser 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/addNoteToFolder.graphql: -------------------------------------------------------------------------------- 1 | mutation addNoteToFolder($folderId: Int!, $noteId: Int!) { 2 | addNoteToFolder(folderId: $folderId, noteId: $noteId) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/changePassword.graphql: -------------------------------------------------------------------------------- 1 | mutation changePassword($token: String!, $newPassword: String!) { 2 | changePassword(token: $token, newPassword: $newPassword) { 3 | ...RegularUserResponse 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/createFolder.graphql: -------------------------------------------------------------------------------- 1 | mutation createFolder($name: String!) { 2 | createFolder(name: $name) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/createNote.graphql: -------------------------------------------------------------------------------- 1 | mutation createNote($title: String!) { 2 | createNote(title: $title) { 3 | ...RegularNote 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/deleteFolder.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteFolder($id: Int!) { 2 | deleteFolder(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/deleteNote.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteNote($id: Int!) { 2 | deleteNote(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/deleteNoteFromFolder.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteNoteFromFolder($folderId: Int!, $noteId: Int!) { 2 | deleteNoteFromFolder(folderId: $folderId, noteId: $noteId) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/forgotPassword.graphql: -------------------------------------------------------------------------------- 1 | mutation ForgotPassword($email: String!) { 2 | forgotPassword(email: $email) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/login.graphql: -------------------------------------------------------------------------------- 1 | mutation Login($email: String!, $password: String!) { 2 | login(email: $email, password: $password) { 3 | ...RegularUserResponse 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/logout.graphql: -------------------------------------------------------------------------------- 1 | mutation Logout { 2 | logout 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/register.graphql: -------------------------------------------------------------------------------- 1 | mutation Register($options: UserInput!) { 2 | register(options: $options) { 3 | ...RegularUserResponse 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/updateName.graphql: -------------------------------------------------------------------------------- 1 | mutation updateName($name: String!) { 2 | updateName(name: $name) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/updateNote.graphql: -------------------------------------------------------------------------------- 1 | mutation updateNote($id: Int!, $body: String!) { 2 | updateNote(id: $id, body: $body) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/mutations/updateNoteTitle.graphql: -------------------------------------------------------------------------------- 1 | mutation updateNoteTitle($id: Int!, $title: String!) { 2 | updateNoteTitle(id: $id, title: $title) 3 | } 4 | -------------------------------------------------------------------------------- /web/src/graphql/queries/getNote.graphql: -------------------------------------------------------------------------------- 1 | query getNote($id: Int!) { 2 | getNote(id: $id) { 3 | ...RegularNote 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/graphql/queries/me.graphql: -------------------------------------------------------------------------------- 1 | query Me { 2 | me { 3 | ...RegularUser 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"; 4 | 5 | const client = new ApolloClient({ 6 | uri: `${process.env.NEXT_PUBLIC_API_URL}/graphql`, 7 | credentials: "include", 8 | cache: new InMemoryCache(), 9 | }); 10 | 11 | function MyApp({ Component, pageProps }: AppProps) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default MyApp; 20 | -------------------------------------------------------------------------------- /web/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx: any) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return { ...initialProps }; 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 17 | 21 | 25 | 29 | 33 | 34 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export default MyDocument; 49 | -------------------------------------------------------------------------------- /web/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /web/src/pages/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Wrapper } from "../../components/shared/Wrapper"; 3 | import { Welcome } from "../../components/ui/Welcome"; 4 | import Head from "next/head"; 5 | import { Meta } from "../../components/shared/Meta"; 6 | import { useIsAuth } from "../../utils/useIsAuth"; 7 | 8 | interface indexProps {} 9 | 10 | const Index: React.FC = ({}) => { 11 | useIsAuth(); 12 | return ( 13 | <> 14 | 15 | 16 | Home - Dino 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default Index; 26 | -------------------------------------------------------------------------------- /web/src/pages/app/n/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React from "react"; 3 | import { Spinner } from "../../../components/shared/Spinner"; 4 | import { Wrapper } from "../../../components/shared/Wrapper"; 5 | import { Editor } from "../../../components/ui/editor/editor"; 6 | import { useGetNoteQuery } from "../../../generated/graphql"; 7 | import Head from "next/head"; 8 | import { Meta } from "../../../components/shared/Meta"; 9 | import { useIsAuth } from "../../../utils/useIsAuth"; 10 | 11 | interface NotePageProps {} 12 | 13 | const NotePage: React.FC = ({}) => { 14 | useIsAuth(); 15 | const router = useRouter(); 16 | const id = 17 | typeof router.query.id == "string" ? parseInt(router.query.id) : -1; 18 | const { data, loading } = useGetNoteQuery({ 19 | variables: { 20 | id, 21 | }, 22 | }); 23 | return ( 24 | <> 25 | 26 | 27 | {data?.getNote.title} - Dino 28 | 29 | 30 | {data && !loading ? ( 31 | 32 | ) : ( 33 |
34 | 35 |
36 | )} 37 |
38 | 39 | ); 40 | }; 41 | 42 | export default NotePage; 43 | -------------------------------------------------------------------------------- /web/src/pages/changepass/[id].tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Form, Formik } from "formik"; 3 | import { InputField } from "../../components/ui/InputField"; 4 | import { useChangePasswordMutation } from "../../generated/graphql"; 5 | import { toErrorMap } from "../../utils/toErrorMap"; 6 | import NextLink from "next/link"; 7 | import { useRouter } from "next/router"; 8 | import Head from "next/head"; 9 | import { Meta } from "../../components/shared/Meta"; 10 | 11 | interface ChangePasswordProps {} 12 | 13 | const ChangePassword: React.FC = ({}) => { 14 | const [tokenError, setTokenError] = useState(""); 15 | const router = useRouter(); 16 | const [changePasswordMutation] = useChangePasswordMutation(); 17 | return ( 18 | <> 19 | 20 | 21 | Change Password - Dino 22 | 23 |
24 |
25 |

26 | Change password 27 |

28 | { 31 | const response = await changePasswordMutation({ 32 | variables: { 33 | newPassword: values.newPassword, 34 | token: 35 | typeof router.query.id === "string" 36 | ? router.query.id 37 | : "", 38 | }, 39 | }); 40 | if (response.data?.changePassword.errors) { 41 | const errorMap = toErrorMap( 42 | response.data.changePassword.errors 43 | ); 44 | if ("token" in errorMap) { 45 | setTokenError(errorMap.token); 46 | } 47 | setErrors(errorMap); 48 | console.log(errorMap); 49 | } else if (response.data?.changePassword.user) { 50 | console.log("here..."); 51 | // login worked 52 | router.push("/app"); 53 | } 54 | }} 55 | > 56 | {({ isSubmitting }) => ( 57 |
58 | 63 | {tokenError ? ( 64 |
65 |

{tokenError}

66 | 67 | Click here to get a new one 68 | 69 |
70 | ) : null} 71 | 78 | 79 | )} 80 |
81 |
82 |
83 | 84 | ); 85 | }; 86 | 87 | export default ChangePassword; 88 | -------------------------------------------------------------------------------- /web/src/pages/forgotpass.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useForgotPasswordMutation } from "../generated/graphql"; 3 | 4 | interface forgotpassProps {} 5 | 6 | const Forgotpass: React.FC = ({}) => { 7 | const [email, setEmail] = useState(""); 8 | const [sentEmailLink, setSentEmailLink] = useState(false); 9 | const [forgotPasswordMut] = useForgotPasswordMutation(); 10 | 11 | const forgotPassword = async () => { 12 | await forgotPasswordMut({ 13 | variables: { 14 | email, 15 | }, 16 | }); 17 | setSentEmailLink(true); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |

24 | Forgot password 25 |

26 | {sentEmailLink && ( 27 |

28 | We{"'"}ve sent an email with a link to change your 29 | password! 30 |

31 | )} 32 |

Email

33 | { 37 | setEmail(e.target.value); 38 | }} 39 | /> 40 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Forgotpass; 52 | -------------------------------------------------------------------------------- /web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unknown-property */ 2 | import type { NextPage } from "next"; 3 | import { useMeQuery } from "../generated/graphql"; 4 | import { ShowcaseNavbar } from "../components/showcase/Navbar"; 5 | import NextLink from "next/link"; 6 | import { useRouter } from "next/router"; 7 | import Head from "next/head"; 8 | 9 | const Home: NextPage = () => { 10 | const { data, loading } = useMeQuery(); 11 | const router = useRouter(); 12 | if (!loading && data?.me != null) { 13 | router.push("/app"); 14 | } 15 | return ( 16 |
17 | 18 | Dino 19 | 20 | 21 |
22 |
23 |

24 | All your notes. 25 |
everywhere 26 |

27 |

28 | Dino is a new way to jot down your thoughts 29 | and all the stuff that you want to access easily and 30 | quickly. Cos 31 | {"'"} not everything is about productivity 32 |

33 | 34 | 35 | 38 | 39 | 40 |
41 |
42 | 47 |
48 |
49 |
50 |
51 | 56 |

57 | Easy to use 58 |

59 |

60 | Unlike other tools, Dino has a zero 61 | learning curve which means you can start using it 62 | without any confusion 63 |

64 |
65 |
66 | 72 |
73 |
74 |
75 |
76 | 81 |

82 | Clean and organised 83 |

84 |

85 | Dino user interface is clean, minimal and 86 | organised so you only get to see your docs when you want 87 | to 88 |

89 |
90 |
91 | {/* */} 96 | 101 |
102 |
103 |
104 | 112 | 113 | 120 | 126 | 132 | 138 | 144 | 150 | 156 | 162 | 168 | 176 | 177 | 178 | 187 | 191 | 197 | 198 | 199 | 200 | 204 | 209 | 215 | 216 | 217 | 218 | 219 |

220 | Try Dino today 221 |

222 | 223 | 224 | 227 | 228 | 229 |
230 | 290 |
291 | ); 292 | }; 293 | 294 | export default Home; 295 | -------------------------------------------------------------------------------- /web/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { useApolloClient } from "@apollo/client"; 2 | import { Form, Formik } from "formik"; 3 | import { useRouter } from "next/router"; 4 | import React, { useState } from "react"; 5 | import { InputField } from "../components/ui/InputField"; 6 | import { 7 | useForgotPasswordMutation, 8 | useLoginMutation, 9 | } from "../generated/graphql"; 10 | import { toErrorMap } from "../utils/toErrorMap"; 11 | 12 | interface LoginProps {} 13 | 14 | const Login: React.FC = ({}) => { 15 | const [loginMut] = useLoginMutation(); 16 | const router = useRouter(); 17 | const client = useApolloClient(); 18 | 19 | return ( 20 |
21 |
22 |

23 | Login 24 |

25 | { 28 | const response = await loginMut({ variables: values }); 29 | if (response.data?.login.errors) { 30 | setErrors(toErrorMap(response.data.login.errors)); 31 | } else if (response.data?.login.user) { 32 | if (typeof router.query.next === "string") { 33 | router.push(router.query.next); 34 | } else { 35 | // worked 36 | await client.resetStore(); 37 | router.push("/app"); 38 | } 39 | } 40 | }} 41 | > 42 | {({ isSubmitting }) => ( 43 |
44 | 49 | 55 | 62 |

63 | Don{"'"}t have an account?{" "} 64 | router.push("/register")} 67 | > 68 | Sign up 69 | 70 |

71 |

router.push("/forgotpass")} 73 | className="mt-3 ml-auto mr-0 text-sm font-medium text-gray-700 cursor-pointer" 74 | > 75 | Forgot password 76 |

77 | 78 | )} 79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | export default Login; 86 | -------------------------------------------------------------------------------- /web/src/pages/product.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unknown-property */ 2 | import React from "react"; 3 | import { ShowcaseNavbar } from "../components/showcase/Navbar"; 4 | import NextLink from "next/link"; 5 | import Head from "next/head"; 6 | 7 | interface productProps {} 8 | 9 | const Product: React.FC = ({}) => { 10 | return ( 11 |
12 | 13 | Dino 14 | 15 | 16 |
17 |
18 |

19 | Simple and smart. 20 |

21 |

22 | Dino is a new way to jot down your thoughts 23 | and all the stuff that you want to access easily and 24 | quickly. Cos 25 | {"'"} not everything is about productivity 26 |

27 | 28 | 29 | 32 | 33 | 34 |
35 |
36 | 41 |
42 |
43 |
44 |
45 | 50 |

51 | Easy to use 52 |

53 |

54 | Unlike other tools, Dino has a zero 55 | learning curve which means you can start using it 56 | without any confusion 57 |

58 |
59 |
60 | 66 |
67 |
68 |
69 |
70 | 75 |

76 | Clean and organised 77 |

78 |

79 | Dino user interface is clean, minimal and 80 | organised so you only get to see your docs when you want 81 | to 82 |

83 |
84 |
85 | {/* */} 90 | 95 |
96 |
97 |
98 | 106 | 107 | 114 | 120 | 126 | 132 | 138 | 144 | 150 | 156 | 162 | 170 | 171 | 172 | 181 | 185 | 191 | 192 | 193 | 194 | 198 | 203 | 209 | 210 | 211 | 212 | 213 |

214 | Try Dino today 215 |

216 | 217 | 218 | 221 | 222 | 223 |
224 | 284 |
285 | ); 286 | }; 287 | 288 | export default Product; 289 | -------------------------------------------------------------------------------- /web/src/pages/register.tsx: -------------------------------------------------------------------------------- 1 | import { useApolloClient } from "@apollo/client"; 2 | import { Form, Formik } from "formik"; 3 | import { useRouter } from "next/router"; 4 | import React from "react"; 5 | import { InputField } from "../components/ui/InputField"; 6 | import { useRegisterMutation } from "../generated/graphql"; 7 | import { toErrorMap } from "../utils/toErrorMap"; 8 | 9 | interface registerProps {} 10 | 11 | const Register: React.FC = ({}) => { 12 | const [registerMut] = useRegisterMutation(); 13 | const router = useRouter(); 14 | const client = useApolloClient(); 15 | return ( 16 |
17 |
18 |

19 | Register 20 |

21 | { 24 | const res = await registerMut({ 25 | variables: { 26 | options: values, 27 | }, 28 | }); 29 | if (res.data?.register.errors) { 30 | setErrors(toErrorMap(res.data.register.errors)); 31 | } else if (res.data?.register.user) { 32 | router.push("/app"); 33 | await client.resetStore(); 34 | } 35 | }} 36 | > 37 | {({ isSubmitting }) => ( 38 |
39 | 44 | 49 | 55 | 62 |

63 | Already have an account?{" "} 64 | router.push("/login")} 67 | > 68 | Login 69 | 70 |

71 | 72 | )} 73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Register; 80 | -------------------------------------------------------------------------------- /web/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japrozs/dino/4298a5e8f43ad6f18df48b41b187ac4049002b13/web/src/public/favicon.ico -------------------------------------------------------------------------------- /web/src/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /web/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://rsms.me/inter/inter.css"); 2 | :root { 3 | --gray-50: #f7fafc; 4 | --gray-100: #edf2f7; 5 | --gray-200: #e2e8f0; 6 | --gray-300: #cbd5e0; 7 | --gray-400: #a0aec0; 8 | --gray-500: #718096; 9 | --gray-600: #4a5568; 10 | --gray-700: #2d3748; 11 | --gray-800: #1a202c; 12 | --gray-900: #171923; 13 | --red-50: #fff5f5; 14 | --red-100: #fed7d7; 15 | --red-200: #feb2b2; 16 | --red-300: #fc8181; 17 | --red-400: #f56565; 18 | --red-500: #e53e3e; 19 | --red-600: #c53030; 20 | --red-700: #9b2c2c; 21 | --red-800: #822727; 22 | --red-900: #63171b; 23 | --orange-50: #fffaf0; 24 | --orange-100: #feebc8; 25 | --orange-200: #fbd38d; 26 | --orange-300: #f6ad55; 27 | --orange-400: #ed8936; 28 | --orange-500: #dd6b20; 29 | --orange-600: #c05621; 30 | --orange-700: #9c4221; 31 | --orange-800: #7b341e; 32 | --orange-900: #652b19; 33 | --yellow-50: #fffff0; 34 | --yellow-100: #fefcbf; 35 | --yellow-200: #faf089; 36 | --yellow-300: #f6e05e; 37 | --yellow-400: #ecc94b; 38 | --yellow-500: #d69e2e; 39 | --yellow-600: #b7791f; 40 | --yellow-700: #975a16; 41 | --yellow-800: #744210; 42 | --yellow-900: #5f370e; 43 | --green-50: #f0fff4; 44 | --green-100: #c6f6d5; 45 | --green-200: #9ae6b4; 46 | --green-300: #68d391; 47 | --green-400: #48bb78; 48 | --green-500: #38a169; 49 | --green-600: #2f855a; 50 | --green-700: #276749; 51 | --green-800: #22543d; 52 | --green-900: #1c4532; 53 | --teal-50: #e6fffa; 54 | --teal-100: #b2f5ea; 55 | --teal-200: #81e6d9; 56 | --teal-300: #4fd1c5; 57 | --teal-400: #38b2ac; 58 | --teal-500: #319795; 59 | --teal-600: #2c7a7b; 60 | --teal-700: #285e61; 61 | --teal-800: #234e52; 62 | --teal-900: #1d4044; 63 | --blue-50: #ebf8ff; 64 | --blue-100: #bee3f8; 65 | --blue-200: #90cdf4; 66 | --blue-300: #63b3ed; 67 | --blue-400: #4299e1; 68 | --blue-500: #3182ce; 69 | --blue-600: #2b6cb0; 70 | --blue-700: #2c5282; 71 | --blue-800: #2a4365; 72 | --blue-900: #1a365d; 73 | --cyan-50: #edfdfd; 74 | --cyan-100: #c4f1f9; 75 | --cyan-200: #9decf9; 76 | --cyan-300: #76e4f7; 77 | --cyan-400: #0bc5ea; 78 | --cyan-500: #00b5d8; 79 | --cyan-600: #00a3c4; 80 | --cyan-700: #0987a0; 81 | --cyan-800: #086f83; 82 | --cyan-900: #065666; 83 | --purple-50: #faf5ff; 84 | --purple-100: #e9d8fd; 85 | --purple-200: #d6bcfa; 86 | --purple-300: #b794f4; 87 | --purple-400: #9f7aea; 88 | --purple-500: #805ad5; 89 | --purple-600: #6b46c1; 90 | --purple-700: #553c9a; 91 | --purple-800: #44337a; 92 | --purple-900: #322659; 93 | --pink-50: #fff5f7; 94 | --pink-100: #fed7e2; 95 | --pink-200: #fbb6ce; 96 | --pink-300: #f687b3; 97 | --pink-400: #ed64a6; 98 | --pink-500: #d53f8c; 99 | --pink-600: #b83280; 100 | --pink-700: #97266d; 101 | --pink-800: #702459; 102 | --pink-900: #521b41; 103 | --linkedin-50: #e8f4f9; 104 | --linkedin-100: #cfedfb; 105 | --linkedin-200: #9bdaf3; 106 | --linkedin-300: #68c7ec; 107 | --linkedin-400: #34b3e4; 108 | --linkedin-500: #00a0dc; 109 | --linkedin-600: #008cc9; 110 | --linkedin-700: #0077b5; 111 | --linkedin-800: #005e93; 112 | --linkedin-900: #004471; 113 | --facebook-50: #e8f4f9; 114 | --facebook-100: #d9dee9; 115 | --facebook-200: #b7c2da; 116 | --facebook-300: #6482c0; 117 | --facebook-400: #4267b2; 118 | --facebook-500: #385898; 119 | --facebook-600: #314e89; 120 | --facebook-700: #29487d; 121 | --facebook-800: #223b67; 122 | --facebook-900: #1e355b; 123 | --messenger-50: #d0e6ff; 124 | --messenger-100: #b9daff; 125 | --messenger-200: #a2cdff; 126 | --messenger-300: #7ab8ff; 127 | --messenger-400: #2e90ff; 128 | --messenger-500: #0078ff; 129 | --messenger-600: #0063d1; 130 | --messenger-700: #0052ac; 131 | --messenger-800: #003c7e; 132 | --messenger-900: #002c5c; 133 | --whatsapp-50: #dffeec; 134 | --whatsapp-100: #b9f5d0; 135 | --whatsapp-200: #90edb3; 136 | --whatsapp-300: #65e495; 137 | --whatsapp-400: #3cdd78; 138 | --whatsapp-500: #22c35e; 139 | --whatsapp-600: #179848; 140 | --whatsapp-700: #0c6c33; 141 | --whatsapp-800: #01421c; 142 | --whatsapp-900: #001803; 143 | --twitter-50: #e5f4fd; 144 | --twitter-100: #c8e9fb; 145 | --twitter-200: #a8dcfa; 146 | --twitter-300: #83cdf7; 147 | --twitter-400: #57bbf5; 148 | --twitter-500: #1da1f2; 149 | --twitter-600: #1a94da; 150 | --twitter-700: #1681bf; 151 | --twitter-800: #136b9e; 152 | --twitter-900: #0d4d71; 153 | --telegram-50: #e3f2f9; 154 | --telegram-100: #c5e4f3; 155 | --telegram-200: #a2d4ec; 156 | --telegram-300: #7ac1e4; 157 | --telegram-400: #47a9da; 158 | --telegram-500: #0088cc; 159 | --telegram-600: #007ab8; 160 | --telegram-700: #006ba1; 161 | --telegram-800: #005885; 162 | --telegram-900: #003f5e; 163 | --spinner-bg: #656565; 164 | --spinner-fg: #b2bac1; 165 | } 166 | 167 | @tailwind base; 168 | @tailwind components; 169 | @tailwind utilities; 170 | html, 171 | body { 172 | padding: 0; 173 | margin: 0; 174 | font-family: "Inter", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 175 | } 176 | 177 | a { 178 | color: inherit; 179 | text-decoration: none; 180 | } 181 | 182 | * { 183 | box-sizing: border-box; 184 | font-family: "Inter" !important; 185 | } 186 | 187 | 188 | /* SPINNER START */ 189 | 190 | .spinner, 191 | .spinner:after { 192 | border-radius: 50%; 193 | width: 10em; 194 | height: 10em; 195 | } 196 | 197 | .spinner-in-button { 198 | margin: 0px !important; 199 | width: 7em; 200 | height: 7em; 201 | border-top: 0.0525rem solid var(--spinner-bg); 202 | border-right: 0.0525rem solid var(--spinner-bg); 203 | border-bottom: 0.0525rem solid var(--spinner-bg); 204 | border-left: 0.0525rem solid var(--spinner-fg); 205 | } 206 | 207 | .spinner { 208 | font-size: 3px; 209 | position: relative; 210 | text-indent: -9999em; 211 | border-top: 0.125rem solid var(--spinner-bg); 212 | border-right: 0.125rem solid var(--spinner-bg); 213 | border-bottom: 0.125rem solid var(--spinner-bg); 214 | border-left: 0.125rem solid var(--spinner-fg); 215 | -webkit-transform: translateZ(0); 216 | -ms-transform: translateZ(0); 217 | transform: translateZ(0); 218 | -webkit-animation: load8 1.1s infinite linear; 219 | animation: load8 1.1s infinite linear; 220 | } 221 | 222 | @-webkit-keyframes load8 { 223 | 0% { 224 | -webkit-transform: rotate(0deg); 225 | transform: rotate(0deg); 226 | } 227 | 100% { 228 | -webkit-transform: rotate(360deg); 229 | transform: rotate(360deg); 230 | } 231 | } 232 | 233 | @keyframes load8 { 234 | 0% { 235 | -webkit-transform: rotate(0deg); 236 | transform: rotate(0deg); 237 | } 238 | 100% { 239 | -webkit-transform: rotate(360deg); 240 | transform: rotate(360deg); 241 | } 242 | } 243 | 244 | 245 | /* SPINNER END */ 246 | 247 | 248 | /* Button start */ 249 | 250 | .button { 251 | padding: 5px 15px; 252 | font-weight: 500; 253 | font-size: 16px; 254 | border-radius: 4px; 255 | background-color: var(--gray-100); 256 | border: none; 257 | cursor: pointer; 258 | transition: background-color 0.1s ease-in-out; 259 | } 260 | 261 | .button:hover { 262 | background-color: var(--gray-200); 263 | } 264 | 265 | 266 | /* Button end */ 267 | 268 | .menlo { 269 | font-family: "Menlo", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; 270 | } 271 | 272 | .no-scrollbar::-webkit-scrollbar { 273 | display: none; 274 | } 275 | 276 | .no-scrollbar { 277 | -ms-overflow-style: none; 278 | /* IE and Edge */ 279 | scrollbar-width: none; 280 | /* Firefox */ 281 | } 282 | 283 | .sidebar { 284 | height: 100vh !important; 285 | position: sticky; 286 | top: 0 !important; 287 | } 288 | 289 | 290 | /* 291 | .editor h1 { 292 | font-size: 2em !important; 293 | } 294 | 295 | .editor h2 { 296 | font-size: 1.5em !important; 297 | } 298 | 299 | .editor h3 { 300 | font-size: 1.17em !important; 301 | } 302 | 303 | .editor h4 { 304 | font-size: 1em !important; 305 | } 306 | 307 | .editor h5 { 308 | font-size: 0.83em !important; 309 | } 310 | 311 | .editor h6 { 312 | font-size: 0.75em !important; 313 | } */ 314 | 315 | @media only screen and (max-width: 780px) { 316 | .sidebar { 317 | display: none !important; 318 | } 319 | } 320 | 321 | code { 322 | font-family: "Menlo", monospace !important; 323 | color: #1e76da !important; 324 | } 325 | 326 | .colored-circle-green { 327 | display: inline-block; 328 | border-radius: 50%; 329 | height: 9px; 330 | width: 9px; 331 | background-color: #28c077; 332 | } 333 | 334 | .colored-circle-gray { 335 | display: inline-block; 336 | border-radius: 50%; 337 | height: 9px; 338 | width: 9px; 339 | background-color: var(--gray-500); 340 | } 341 | 342 | .colored-circle-red { 343 | display: inline-block; 344 | border-radius: 50%; 345 | height: 9px; 346 | width: 9px; 347 | background-color: var(--red-500); 348 | } 349 | 350 | .colored-circle { 351 | display: inline-block; 352 | border-radius: 50%; 353 | height: 9px; 354 | width: 9px; 355 | } 356 | 357 | .editor h1 { 358 | font-size: 1.8em; 359 | font-weight: 500; 360 | margin: 2px 0px; 361 | } 362 | 363 | .editor h2 { 364 | font-size: 1.5em; 365 | font-weight: 600; 366 | } 367 | 368 | .editor h3 { 369 | font-size: 1.17em; 370 | font-weight: 500; 371 | } 372 | 373 | .editor h4 { 374 | font-size: 1em; 375 | font-weight: 500; 376 | } 377 | 378 | .editor h5 { 379 | font-size: 0.83em; 380 | font-weight: 600; 381 | margin: 1.67em 0; 382 | } 383 | 384 | .editor h6 { 385 | font-size: 0.67em; 386 | font-weight: 600; 387 | } 388 | 389 | @media (prefers-color-scheme: dark) { 390 | .editor code * { 391 | font-family: "Menlo" !important; 392 | background-color: #374151; 393 | padding: 2px; 394 | border-radius: 3px; 395 | font-size: 95%; 396 | } 397 | } 398 | 399 | @media (prefers-color-scheme: light) { 400 | .editor code * { 401 | font-family: "Menlo" !important; 402 | background-color: var(--gray-200); 403 | padding: 2px; 404 | border-radius: 3px; 405 | font-size: 95%; 406 | } 407 | } 408 | 409 | .editor .bold { 410 | font-weight: 600; 411 | } 412 | 413 | .editor a { 414 | color: #1e76da; 415 | } -------------------------------------------------------------------------------- /web/src/utils/FindFolderId.ts: -------------------------------------------------------------------------------- 1 | export const findFolderId = (name: string, folders: any[]) => { 2 | let id = -1; 3 | folders.map((folder) => { 4 | if (folder.name == name) { 5 | console.log("here folder"); 6 | id = folder.id; 7 | } 8 | }); 9 | return id; 10 | }; 11 | -------------------------------------------------------------------------------- /web/src/utils/findNoteFolder.ts: -------------------------------------------------------------------------------- 1 | export const findNoteFolder = (id: number, folders: any) => { 2 | let name = "No Folder"; 3 | folders.map((folder: any) => { 4 | console.log("folder.noteIds ::", folder.noteIds); 5 | console.log("id ::", id); 6 | console.log(folder.noteIds.includes(id.toString())); 7 | if (folder.noteIds.includes(id.toString())) { 8 | console.log("here.."); 9 | name = folder.name; 10 | } 11 | }); 12 | 13 | return name; 14 | }; 15 | -------------------------------------------------------------------------------- /web/src/utils/searchNotes.ts: -------------------------------------------------------------------------------- 1 | import { RegularNoteFragment } from "../generated/graphql"; 2 | 3 | export const searchNotes = (query: string, notes: RegularNoteFragment[]) => { 4 | let arr: any[] = []; 5 | notes.map((note) => { 6 | if ( 7 | note.title.trim().toLowerCase().includes(query.trim().toLowerCase()) 8 | ) { 9 | arr.push(note); 10 | } 11 | }); 12 | 13 | return arr; 14 | }; 15 | -------------------------------------------------------------------------------- /web/src/utils/timeSince.ts: -------------------------------------------------------------------------------- 1 | export const timeSince = (date: string | undefined) => { 2 | const dateToString = new Date(parseInt(date || "")).toString(); 3 | const d = new Date(dateToString); 4 | var seconds = Math.floor((new Date().getTime() - d.getTime()) / 1000); 5 | 6 | var interval = seconds / 31536000; 7 | 8 | if (interval > 1) { 9 | return Math.floor(interval) + " years"; 10 | } 11 | interval = seconds / 2592000; 12 | if (interval > 1) { 13 | return Math.floor(interval) + " months"; 14 | } 15 | interval = seconds / 86400; 16 | if (interval > 1) { 17 | return Math.floor(interval) + " days"; 18 | } 19 | interval = seconds / 3600; 20 | if (interval > 1) { 21 | return Math.floor(interval) + " hours"; 22 | } 23 | interval = seconds / 60; 24 | if (interval > 1) { 25 | return Math.floor(interval) + " minutes"; 26 | } 27 | return Math.floor(seconds) + " seconds"; 28 | }; 29 | 30 | export const timeSinceShort = (date: string | undefined) => { 31 | const dateToString = new Date(parseInt(date || "")).toString(); 32 | const d = new Date(dateToString); 33 | var seconds = Math.floor((new Date().getTime() - d.getTime()) / 1000); 34 | 35 | var interval = seconds / 31536000; 36 | 37 | if (interval > 1) { 38 | return Math.floor(interval) + "y "; 39 | } 40 | interval = seconds / 2592000; 41 | if (interval > 1) { 42 | return Math.floor(interval) + "m "; 43 | } 44 | interval = seconds / 86400; 45 | if (interval > 1) { 46 | return Math.floor(interval) + "d"; 47 | } 48 | interval = seconds / 3600; 49 | if (interval > 1) { 50 | return Math.floor(interval) + "h"; 51 | } 52 | interval = seconds / 60; 53 | if (interval > 1) { 54 | return Math.floor(interval) + "m"; 55 | } 56 | return Math.floor(seconds) + "s"; 57 | }; 58 | -------------------------------------------------------------------------------- /web/src/utils/toErrorMap.ts: -------------------------------------------------------------------------------- 1 | import { FieldError } from "../generated/graphql"; 2 | 3 | export const toErrorMap = (errors: FieldError[]) => { 4 | const errorMap: Record = {}; 5 | errors.forEach(({ field, message }) => { 6 | errorMap[field] = message; 7 | }); 8 | 9 | return errorMap; 10 | }; 11 | -------------------------------------------------------------------------------- /web/src/utils/useIsAuth.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { useMeQuery } from "../generated/graphql"; 4 | 5 | export const useIsAuth = () => { 6 | const { data, loading } = useMeQuery(); 7 | const router = useRouter(); 8 | console.log("data ::", data); 9 | useEffect(() => { 10 | if (!loading && !data?.me) { 11 | router.replace("/"); 12 | } 13 | }, [loading, data, router]); 14 | }; 15 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [ 3 | "./src/pages/**/*.{js,ts,jsx,tsx}", 4 | "./src/components/**/*.{js,ts,jsx,tsx}", 5 | "./src/components/**/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | darkMode: "media", // or 'media' or 'class' 8 | theme: { 9 | extend: { 10 | colors: { 11 | "btn-dark": "rgba(235, 87,87, 0.1)", 12 | black: { 13 | 50: "#A4A4A4", 14 | 100: "#929292", 15 | 200: "#808080", 16 | 300: "#6D6D6D", 17 | 400: "#5B5B5B", 18 | 500: "#494949", 19 | 600: "#373737", 20 | 700: "#242424", 21 | 800: "#121212", 22 | 900: "#000000", 23 | pantone: "#101820", 24 | navbar: "#1e1b1b", 25 | }, 26 | }, 27 | }, 28 | }, 29 | variants: { 30 | extend: {}, 31 | }, 32 | plugins: [], 33 | }; -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------