├── client ├── .env.sample ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── grid.png │ │ ├── NotFound.png │ │ ├── Alert.tsx │ │ ├── Loader.tsx │ │ ├── Close.tsx │ │ ├── Copy.tsx │ │ ├── Speedial.tsx │ │ ├── Image.tsx │ │ └── AuthGraphic.tsx │ ├── lib │ │ ├── constants.ts │ │ ├── image.ts │ │ └── signupFormOptions.ts │ ├── state │ │ ├── token.ts │ │ └── image.ts │ ├── index.css │ ├── pages │ │ ├── Auth.tsx │ │ ├── index.tsx │ │ └── NotFound.tsx │ ├── schem │ │ └── image.ts │ ├── main.tsx │ ├── App.tsx │ └── components │ │ ├── Upload │ │ ├── index.tsx │ │ └── UploadFile.tsx │ │ ├── DeleteImage │ │ ├── index.tsx │ │ └── Delete.tsx │ │ ├── Navbar │ │ ├── User.tsx │ │ └── index.tsx │ │ ├── EditImage │ │ ├── DrawingText │ │ │ ├── index.tsx │ │ │ └── circle.tsx │ │ ├── FileOperations │ │ │ ├── index.tsx │ │ │ └── quality.tsx │ │ ├── FiltersEffect │ │ │ ├── index.tsx │ │ │ ├── blur.tsx │ │ │ ├── gaussian.tsx │ │ │ └── pixelate.tsx │ │ ├── BasicImageManipulation │ │ │ ├── index.tsx │ │ │ ├── width.tsx │ │ │ ├── height.tsx │ │ │ ├── rotate.tsx │ │ │ ├── flip.tsx │ │ │ ├── xflip.tsx │ │ │ ├── yflip.tsx │ │ │ └── crop.tsx │ │ ├── Accordian.tsx │ │ ├── PublicToggle.tsx │ │ ├── ColorAdjustments │ │ │ ├── fase.tsx │ │ │ ├── contrast.tsx │ │ │ ├── index.tsx │ │ │ ├── brightness.tsx │ │ │ ├── sepia.tsx │ │ │ ├── invert.tsx │ │ │ ├── dither565.tsx │ │ │ ├── greyscale.tsx │ │ │ ├── normalize.tsx │ │ │ └── posterize.tsx │ │ ├── DraggableImage.tsx │ │ ├── index.tsx │ │ ├── EditAccordian.tsx │ │ └── base.ts │ │ ├── Login │ │ ├── Layout.tsx │ │ └── index.tsx │ │ ├── Signup │ │ ├── Layout.tsx │ │ └── index.tsx │ │ ├── Dashboard │ │ ├── index.tsx │ │ ├── Images.tsx │ │ └── Image.tsx │ │ └── SpeedDial.tsx ├── favicon.png ├── postcss.config.js ├── vercel.json ├── tsconfig.json ├── Dockerfile ├── vite.config.ts ├── tsconfig.node.json ├── index.html ├── .eslintrc.cjs ├── tailwind.config.js ├── tsconfig.app.json ├── README.md └── package.json ├── server ├── .dockerignore ├── prisma │ ├── migrations │ │ ├── migration_lock.toml │ │ └── 20240920173915_db_models │ │ │ └── migration.sql │ └── schema.prisma ├── Dockerfile ├── src │ ├── routes │ │ ├── index.ts │ │ ├── auth.ts │ │ └── image.ts │ ├── constants │ │ ├── Image │ │ │ ├── index.ts │ │ │ ├── aws.ts │ │ │ └── cloudinary.ts │ │ └── image.ts │ ├── client.ts │ ├── schema │ │ ├── user.ts │ │ └── image.ts │ ├── lib │ │ ├── user.ts │ │ ├── image.ts │ │ ├── constants.ts │ │ └── pass.ts │ ├── index.ts │ ├── middlewares.ts │ ├── multerConfig.ts │ ├── controllers │ │ ├── user.ts │ │ └── image.ts │ ├── db │ │ └── user.ts │ └── utils │ │ └── image.ts ├── .env.sample ├── package.json └── tsconfig.json ├── docker-compose.yml ├── .gitignore ├── dev.docker-compose.yml └── Readme.md /client/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL= -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulkitxm/image-tweaker/HEAD/client/favicon.png -------------------------------------------------------------------------------- /client/src/assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulkitxm/image-tweaker/HEAD/client/src/assets/grid.png -------------------------------------------------------------------------------- /client/src/assets/NotFound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulkitxm/image-tweaker/HEAD/client/src/assets/NotFound.png -------------------------------------------------------------------------------- /client/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const BACKEND_URL = 2 | import.meta.env.VITE_BACKEND_URL || "http://localhost:3000"; -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /client/src/state/token.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "recoil"; 2 | 3 | export const tokenState = atom({ 4 | key: "token", 5 | default: undefined, 6 | }); 7 | -------------------------------------------------------------------------------- /server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body{ 6 | background-color: #f3f4f6; 7 | overflow: hidden; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 3000 14 | 15 | CMD ["npm", "start"] 16 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | host: true, 8 | port: 5173, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | COPY prisma ./prisma 8 | 9 | RUN npm install 10 | 11 | RUN npx prisma generate 12 | 13 | COPY . . 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /client/src/pages/Auth.tsx: -------------------------------------------------------------------------------- 1 | import Login from "../components/Login"; 2 | import Signup from "../components/Signup"; 3 | 4 | export default function auth(type: "login" | "signup") { 5 | if (type === "login") { 6 | return ; 7 | } else { 8 | return ; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authRouter from "./auth"; 3 | import imageRouter from "./image"; 4 | 5 | const routesHandler= Router(); 6 | 7 | routesHandler.use('/api/auth', authRouter); 8 | routesHandler.use('/api/image', imageRouter); 9 | 10 | export default routesHandler; -------------------------------------------------------------------------------- /client/src/schem/image.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const validateImage = z.object({ 4 | isPublic: z.boolean(), 5 | id: z.string(), 6 | isOwner: z.boolean(), 7 | }); 8 | 9 | export const validateImageFetch = z.array(validateImage); 10 | 11 | export type Image = z.infer; 12 | export type Images = z.infer; -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import { RecoilRoot } from "recoil"; 5 | 6 | import "react-contexify/dist/ReactContexify.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # client: 3 | # build: 4 | # context: ./client 5 | # dockerfile: Dockerfile 6 | # ports: 7 | # - "5173:3000" 8 | # depends_on: 9 | # - server 10 | # env_file: 11 | # - ./client/.env 12 | 13 | server: 14 | build: 15 | context: ./server 16 | dockerfile: Dockerfile 17 | ports: 18 | - "3001:3000" 19 | env_file: 20 | - ./server/.env 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | .cmds 27 | node_modules/ 28 | dist/ 29 | images/ -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Pages from "./pages"; 2 | import "./index.css"; 3 | import { useEffect } from "react"; 4 | import { useSetRecoilState } from "recoil"; 5 | import { tokenState } from "./state/token"; 6 | 7 | export default function App() { 8 | const setToken = useSetRecoilState(tokenState); 9 | useEffect(() => { 10 | const token = localStorage.getItem("token"); 11 | setToken(token ?? undefined); 12 | }, [setToken]); 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /server/src/constants/Image/index.ts: -------------------------------------------------------------------------------- 1 | import Jimp from "jimp"; 2 | 3 | export default abstract class Image { 4 | public imageId: string | undefined = undefined; 5 | abstract uploadImage({ 6 | code, 7 | dbId, 8 | image, 9 | }: { 10 | image: Jimp | Buffer; 11 | dbId: string; 12 | code: string; 13 | }): Promise; 14 | abstract deleteImage(imageKey: string): Promise; 15 | abstract downloadImage(imageKey: string): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/assets/Alert.tsx: -------------------------------------------------------------------------------- 1 | export default function Alert() { 2 | return ( 3 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/state/image.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "recoil"; 2 | import { Images } from "../schem/image"; 3 | 4 | export const imagesState = atom({ 5 | key: "images", 6 | default: [], 7 | }); 8 | 9 | export const deleteImageConfigState = atom({ 10 | key: "imagePreviewCntrl", 11 | default: { 12 | open: false, 13 | imageId: "", 14 | imageUrl: "", 15 | }, 16 | }); 17 | 18 | export const openUploadDialogState = atom({ 19 | key: "openUploadDialog", 20 | default: false, 21 | }); 22 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Tweaker: Your own Image Editor 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/Upload/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRecoilState } from "recoil"; 2 | import { openUploadDialogState } from "../../state/image"; 3 | import UploadFile from "./UploadFile"; 4 | 5 | export default function UploadDialog() { 6 | const [open, setOpen] = useRecoilState(openUploadDialogState); 7 | if (open) 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: "#00df9a", 8 | }, 9 | boxShadow: { 10 | custom: "rgba(255, 255, 255, 0.1) 0px 1px 1px 0px inset, rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px", 11 | custom2: "rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px", 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @default(cuid()) 12 | email String @unique 13 | username String @unique 14 | password String 15 | images Image[] 16 | } 17 | 18 | model Image { 19 | id String @id @default(cuid()) 20 | imageKey String 21 | createdBy User @relation(fields: [createdById], references: [id]) 22 | createdById String 23 | isPublic Boolean @default(false) 24 | } 25 | -------------------------------------------------------------------------------- /server/src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { handleLoginUser, handleSignupUser } from "../controllers/user"; 3 | import rateLimit from "express-rate-limit"; 4 | 5 | const authRouter = Router(); 6 | 7 | const limiter = rateLimit({ 8 | windowMs: 15 * 60 * 1000, 9 | max: 5, 10 | standardHeaders: true, 11 | legacyHeaders: false, 12 | message: "Too many requests, please try again later.", 13 | }); 14 | 15 | authRouter.use(limiter); 16 | 17 | authRouter.post("/register", handleSignupUser); 18 | authRouter.post("/login", handleLoginUser); 19 | 20 | export default authRouter; -------------------------------------------------------------------------------- /server/src/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { NODE_ENV } from "./lib/constants"; 3 | 4 | const prismaClientSingleton = (() => { 5 | let prisma: PrismaClient | null = null; 6 | 7 | return () => { 8 | if (!prisma) { 9 | prisma = new PrismaClient(); 10 | } 11 | return prisma; 12 | }; 13 | })(); 14 | 15 | declare global { 16 | var prismaGlobal: PrismaClient | undefined; 17 | } 18 | 19 | const prisma = global.prismaGlobal ?? prismaClientSingleton(); 20 | 21 | if (NODE_ENV !== "production") { 22 | global.prismaGlobal = prisma; 23 | } 24 | 25 | export default prisma; 26 | -------------------------------------------------------------------------------- /server/src/schema/user.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const UserSchema = z.object({ 4 | id: z.string().uuid(), 5 | username: z.string().min(1), 6 | email: z.string().email(), 7 | }); 8 | 9 | export const UserCreateSchema = z.object({ 10 | username: z.string().min(1), 11 | email: z.string().email(), 12 | password: z.string().min(8), 13 | }); 14 | 15 | export const UserLoginSchema = z.object({ 16 | username: z.string().min(1), 17 | password: z.string().min(8), 18 | }); 19 | 20 | export type TokenType = { 21 | username: string; 22 | password: string; 23 | }; 24 | export type UserType= z.infer; -------------------------------------------------------------------------------- /client/src/assets/Loader.tsx: -------------------------------------------------------------------------------- 1 | export default function Loader({ 2 | className, 3 | }: { 4 | className?: string; 5 | } = {}) { 6 | return ( 7 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /server/.env.sample: -------------------------------------------------------------------------------- 1 | CLIENT_URL= 2 | PORT= 3 | 4 | AWS_S3_BUCKET= 5 | AWS_ACCESS_KEY_ID= 6 | AWS_SECRET= 7 | AWS_REGION= 8 | 9 | CLOUDINARY_CLOUD_NAME= 10 | CLOUDINARY_API_KEY= 11 | CLOUDINARY_API_SECRET= 12 | 13 | UPLOAD_ENV= 14 | 15 | POSTGRES_USER= 16 | POSTGRES_PASSWORD= 17 | POSTGRES_DB= 18 | PGWEB_DATABASE_URL= 19 | DATABASE_URL= 20 | 21 | JWT_SECRET= 22 | HASH_PREFIX= 23 | HASH_SUFFIX= 24 | HASH_SALT= 25 | 26 | PGADMIN_DEFAULT_EMAIL= 27 | PGADMIN_DEFAULT_PASSWORD= -------------------------------------------------------------------------------- /client/src/components/DeleteImage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRecoilState } from "recoil"; 2 | import { deleteImageConfigState } from "../../state/image"; 3 | import Delete from "./Delete"; 4 | 5 | export default function DeleteImage() { 6 | const [deleteImageConfig, setDeleteImageConfig] = useRecoilState( 7 | deleteImageConfigState 8 | ); 9 | if (deleteImageConfig.open && deleteImageConfig.imageId) 10 | return ( 11 |
12 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /server/src/lib/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { decodeToken } from "./pass"; 3 | import prisma from "../client"; 4 | 5 | export async function checkToken(req: Request, resp: Response) { 6 | const authHeader = req.headers.authorization; 7 | const token = authHeader?.split(" ")[1]; 8 | if (!token) { 9 | return null; 10 | } 11 | try { 12 | const { username } = decodeToken(token); 13 | const userExists = await prisma.user.findUnique({ 14 | where: { 15 | username: username, 16 | }, 17 | }); 18 | return userExists ? userExists.id : null; 19 | } catch (error) { 20 | return resp.status(401).send({ 21 | message: "Unauthorized Access, Please login to continue", 22 | status: "failed", 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/Navbar/User.tsx: -------------------------------------------------------------------------------- 1 | import { PopoverContent } from "@material-tailwind/react"; 2 | import { useSetRecoilState } from "recoil"; 3 | import { tokenState } from "../../state/token"; 4 | 5 | export function User({ 6 | triggers, 7 | }: { 8 | triggers: { 9 | onMouseEnter: () => void; 10 | onMouseLeave: () => void; 11 | }; 12 | }) { 13 | const setCookie = useSetRecoilState(tokenState); 14 | function logOut() { 15 | setCookie(""); 16 | localStorage.removeItem("token"); 17 | } 18 | return ( 19 | // @ts-expect-error This is a popover content 20 | 25 | Logout 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/EditImage/DrawingText/index.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from "../Accordian"; 2 | import { FiltersType } from "../base"; 3 | import Circle from "./circle"; 4 | 5 | export default function DrawingText({ 6 | title, 7 | changeAccordionState, 8 | currTitle, 9 | filterIndex, 10 | filters, 11 | setFilters, 12 | }: { 13 | title: string; 14 | changeAccordionState: (title: string) => void; 15 | currTitle: string; 16 | filterIndex: number; 17 | filters: FiltersType; 18 | setFilters: React.Dispatch>; 19 | }) { 20 | return ( 21 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FileOperations/index.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from "../Accordian"; 2 | import { FiltersType } from "../base"; 3 | import Quality from "./quality"; 4 | 5 | export default function FileOperations({ 6 | title, 7 | changeAccordionState, 8 | currTitle, 9 | filterIndex, 10 | filters, 11 | setFilters, 12 | }: { 13 | title: string; 14 | changeAccordionState: (title: string) => void; 15 | currTitle: string; 16 | filterIndex: number; 17 | filters: FiltersType; 18 | setFilters: React.Dispatch>; 19 | }) { 20 | return ( 21 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /dev.docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | client: 3 | build: 4 | context: ./client 5 | dockerfile: Dockerfile 6 | ports: 7 | - "5173:3000" 8 | depends_on: 9 | - server 10 | env_file: 11 | - ./client/.env 12 | 13 | server: 14 | build: 15 | context: ./server 16 | dockerfile: Dockerfile 17 | ports: 18 | - "3000:3000" 19 | env_file: 20 | - ./server/.env 21 | 22 | postgres: 23 | image: postgres 24 | ports: 25 | - "5432:5432" 26 | env_file: 27 | - ./server/.env 28 | volumes: 29 | - postgres_data_dwl:/var/lib/postgresql/data 30 | pgweb: 31 | restart: always 32 | image: sosedoff/pgweb 33 | ports: 34 | - "8002:8081" 35 | links: 36 | - postgres:postgres 37 | env_file: 38 | - ./server/.env 39 | depends_on: 40 | - postgres 41 | 42 | volumes: 43 | postgres_data_dwl: -------------------------------------------------------------------------------- /server/prisma/migrations/20240920173915_db_models/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "username" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | 8 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "Image" ( 13 | "id" TEXT NOT NULL, 14 | "imageKey" TEXT NOT NULL, 15 | "createdById" TEXT NOT NULL, 16 | "isPublic" BOOLEAN NOT NULL DEFAULT false, 17 | 18 | CONSTRAINT "Image_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 23 | 24 | -- CreateIndex 25 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Image" ADD CONSTRAINT "Image_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /server/src/lib/image.ts: -------------------------------------------------------------------------------- 1 | import Jimp from "jimp"; 2 | import QueryString from "qs"; 3 | import { manipulateImage, sortQueryParamns } from "../utils/image"; 4 | import { CloudinaryImage } from "../constants/Image/cloudinary"; 5 | 6 | export async function handleManipulateImage( 7 | image: Buffer, 8 | searchParams: QueryString.ParsedQs, 9 | dbId: string 10 | ): Promise { 11 | try { 12 | const { sortedParams, code } = sortQueryParamns(searchParams); 13 | const img = await Jimp.read(image); 14 | const manipulatedImage = manipulateImage(img, sortedParams); 15 | if (code) { 16 | const image = new CloudinaryImage(); 17 | await image.uploadImage({ 18 | image: manipulatedImage, 19 | code, 20 | dbId, 21 | }); 22 | } 23 | return manipulatedImage; 24 | } catch (error) { 25 | console.log(error); 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | import cors from "cors"; 4 | import routesHandler from "./routes"; 5 | import cookieParser from "cookie-parser"; 6 | import prisma from "./client"; 7 | import { CLIENT_URL, PORT as PortToRun } from "./lib/constants"; 8 | 9 | const app = express(); 10 | app.use(express.json()); 11 | app.use(cookieParser()); 12 | app.use( 13 | cors({ 14 | origin: CLIENT_URL, 15 | credentials: true, 16 | }) 17 | ); 18 | app.use( 19 | morgan(":method :url :status :res[content-length] - :response-time ms") 20 | ); 21 | app.use("/", routesHandler); 22 | 23 | const PORT = PortToRun || 3000; 24 | app 25 | .listen(PORT, async () => { 26 | await prisma.$connect(); 27 | console.log("Database connected"); 28 | console.log("App is running at PORT:", PORT); 29 | }) 30 | .on("error", (error) => { 31 | throw new Error(error.message); 32 | }); 33 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FiltersEffect/index.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from "../Accordian"; 2 | import { FiltersType } from "../base"; 3 | import Blur from "./blur"; 4 | import Gaussian from "./gaussian"; 5 | import Pixelate from "./pixelate"; 6 | 7 | export default function FiltersEffects({ 8 | title, 9 | changeAccordionState, 10 | currTitle, 11 | filterIndex, 12 | filters, 13 | setFilters, 14 | }: { 15 | title: string; 16 | changeAccordionState: (title: string) => void; 17 | currTitle: string; 18 | filterIndex: number; 19 | filters: FiltersType; 20 | setFilters: React.Dispatch>; 21 | }) { 22 | return ( 23 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | import auth from "./Auth"; 3 | import Dashboard from "../components/Dashboard"; 4 | import SpeedDial from "../components/SpeedDial"; 5 | import { Toaster } from "react-hot-toast"; 6 | import Navbar from "../components/Navbar"; 7 | import UploadDialog from "../components/Upload"; 8 | import DeleteImage from "../components/DeleteImage"; 9 | import EditImage from "../components/EditImage"; 10 | import NotFound from "./NotFound"; 11 | 12 | export default function Pages() { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | } /> 22 | } /> 23 | } /> 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default function NotFound() { 4 | const previosPageUrl = document.referrer; 5 | return ( 6 |
7 |

8 | 404 9 |

10 |
11 | Page Not Found 12 |
13 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /server/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import prisma from "./client"; 3 | import { decodeToken } from "./lib/pass"; 4 | 5 | export async function checkToken( 6 | req: Request, 7 | resp: Response, 8 | next: NextFunction 9 | ) { 10 | const authHeader = req.headers.authorization; 11 | const token = authHeader?.split(" ")[1]; 12 | if (!token) { 13 | return resp.status(401).send({ 14 | message: "Unauthorized Access, Please login to continue", 15 | status: "failed", 16 | }); 17 | } 18 | try { 19 | const { username } = decodeToken(token); 20 | const userExists = await prisma.user.findFirst({ 21 | where: { 22 | username, 23 | }, 24 | }); 25 | if (!userExists) { 26 | return resp.status(401).send({ 27 | message: "Unauthorized Access, Please login to continue", 28 | status: "failed", 29 | }); 30 | } 31 | resp.locals.user = userExists; 32 | next(); 33 | } catch (error) { 34 | return resp.status(401).send({ 35 | message: "Unauthorized Access, Please login to continue", 36 | status: "failed", 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | export const CLIENT_URL = process.env.CLIENT_URL || "http://localhost:5173"; 5 | export const PORT = process.env.PORT || 5173; 6 | export const NODE_ENV = process.env.NODE_ENV || "development"; 7 | export const HASH_PREFIX = process.env.HASH_PREFIX || "xx"; 8 | export const HASH_SUFFIX = process.env.HASH_SUFFIX || "yy"; 9 | export const HASH_SALT = process.env.HASH_SALT || "zz"; 10 | export const JWT_SECRET = process.env.JWT_SECRET || "secret"; 11 | export const CLOUDINARY_CLOUD_NAME = 12 | process.env.CLOUDINARY_CLOUD_NAME || "cloud_name"; 13 | export const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY || "api_key"; 14 | export const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET || "api_secret"; 15 | 16 | export const AWS_ACCESS_KEY_ID = 17 | process.env.AWS_ACCESS_KEY_ID || "access_key_id"; 18 | export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET || "secret"; 19 | export const AWS_S3_BUCKET = process.env.AWS_S3_BUCKET || "bucket"; 20 | export const AWS_REGION = process.env.AWS_REGION || "region"; 21 | export const AWS_S3_URL = `https://${AWS_S3_BUCKET}.s3.${AWS_REGION}.amazonaws.com`; 22 | export const UPLOAD_ENV = process.env.UPLOAD_ENV || "aws"; 23 | -------------------------------------------------------------------------------- /server/src/routes/image.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import upload, { handleMulterErrors } from "../multerConfig"; 3 | import { 4 | addImage, 5 | getImageById, 6 | getImageDetails, 7 | getImages, 8 | handleChangeImagePrivacy, 9 | handleDeleteImage, 10 | handleGetImagePrivacyStatus, 11 | } from "../controllers/image"; 12 | 13 | import dotenv from "dotenv"; 14 | import { checkToken } from "../middlewares"; 15 | import rateLimit from "express-rate-limit"; 16 | dotenv.config(); 17 | 18 | const imageRouter = Router(); 19 | 20 | const imageRateLimiter = rateLimit({ 21 | windowMs: 15 * 60 * 1000, 22 | max: 30, 23 | standardHeaders: true, 24 | legacyHeaders: false, 25 | message: "Too many requests, please try again later.", 26 | }); 27 | 28 | imageRouter.get("/:public_id", getImageById); 29 | imageRouter.get("/:public_id/details", getImageDetails); 30 | 31 | imageRouter.use(checkToken); 32 | imageRouter.use(imageRateLimiter); 33 | 34 | imageRouter.get("/", getImages); 35 | imageRouter.post("/", upload.single("image"), handleMulterErrors, addImage); 36 | imageRouter.delete("/:public_id", handleDeleteImage); 37 | imageRouter.get("/privacy/:public_id", handleGetImagePrivacyStatus); 38 | imageRouter.patch("/privacy/:public_id", handleChangeImagePrivacy); 39 | 40 | export default imageRouter; 41 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node dist/index.js", 8 | "dev": "nodemon src/index.ts", 9 | "build": "tsc" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@prisma/client": "^5.16.1", 16 | "aws-sdk": "^2.1666.0", 17 | "axios": "^1.7.2", 18 | "cloudinary": "^2.5.0", 19 | "cookie-parser": "^1.4.6", 20 | "cors": "^2.8.5", 21 | "express": "^4.21.0", 22 | "express-rate-limit": "^7.3.1", 23 | "jimp": "^0.22.12", 24 | "jsonwebtoken": "^9.0.2", 25 | "morgan": "^1.10.0", 26 | "multer": "^1.4.5-lts.1", 27 | "multer-s3": "^3.0.1", 28 | "nodemon": "^3.1.4", 29 | "streamifier": "^0.1.1", 30 | "ts-node": "^10.9.2", 31 | "zod": "^3.23.8" 32 | }, 33 | "devDependencies": { 34 | "@types/cookie-parser": "^1.4.7", 35 | "@types/cors": "^2.8.17", 36 | "@types/dotenv": "^8.2.0", 37 | "@types/express": "^4.17.21", 38 | "@types/jsonwebtoken": "^9.0.6", 39 | "@types/morgan": "^1.9.9", 40 | "@types/multer": "^1.4.11", 41 | "@types/multer-s3": "^3.0.3", 42 | "@types/node": "^20.16.5", 43 | "@types/streamifier": "^0.1.2", 44 | "@types/uuid": "^10.0.0", 45 | "prisma": "^5.16.1", 46 | "typescript": "^5.5.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/index.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from "../Accordian"; 2 | import { FiltersType } from "../base"; 3 | import Crop from "./crop"; 4 | import Flip from "./flip"; 5 | import Height from "./height"; 6 | import Rotate from "./rotate"; 7 | import Width from "./width"; 8 | import Xflip from "./xflip"; 9 | import Yflip from "./yflip"; 10 | 11 | export default function BasicImageManipulation({ 12 | title, 13 | changeAccordionState, 14 | currTitle, 15 | filterIndex, 16 | filters, 17 | setFilters, 18 | }: { 19 | title: string; 20 | changeAccordionState: (title: string) => void; 21 | currTitle: string; 22 | filterIndex: number; 23 | filters: FiltersType; 24 | setFilters: React.Dispatch>; 25 | }) { 26 | return ( 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "serve -s dist", 8 | "dev": "vite", 9 | "build": "tsc -b && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@heroicons/react": "^2.1.5", 15 | "@material-tailwind/react": "^2.1.9", 16 | "axios": "^1.7.2", 17 | "moveable-helper": "^0.4.0", 18 | "photoswipe": "^5.4.4", 19 | "react": "^18.3.1", 20 | "react-contexify": "^6.0.0", 21 | "react-dom": "^18.3.1", 22 | "react-hot-toast": "^2.4.1", 23 | "react-moveable": "^0.56.0", 24 | "react-photoswipe-gallery": "^3.0.1", 25 | "react-router-dom": "^6.24.1", 26 | "recoil": "^0.7.7", 27 | "serve": "^14.2.3", 28 | "useeform": "^1.0.8", 29 | "zod": "^3.23.8" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^18.3.3", 33 | "@types/react-dom": "^18.3.0", 34 | "@typescript-eslint/eslint-plugin": "^7.13.1", 35 | "@typescript-eslint/parser": "^7.13.1", 36 | "@vitejs/plugin-react": "^4.3.1", 37 | "autoprefixer": "^10.4.19", 38 | "eslint": "^8.57.0", 39 | "eslint-plugin-react-hooks": "^4.6.2", 40 | "eslint-plugin-react-refresh": "^0.4.7", 41 | "postcss": "^8.4.39", 42 | "tailwindcss": "^3.4.4", 43 | "typescript": "^5.2.2", 44 | "vite": "^5.3.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/EditImage/Accordian.tsx: -------------------------------------------------------------------------------- 1 | const Accordion = ({ 2 | title, 3 | children, 4 | changeAccordionState, 5 | currTitle, 6 | }: { 7 | title: string; 8 | changeAccordionState: (title: string) => void; 9 | children: React.ReactNode; 10 | currTitle: string; 11 | }) => { 12 | const isOpen = currTitle === title; 13 | return ( 14 |
15 |
changeAccordionState(title)} 18 | > 19 |

{title}

20 | 29 | 35 | 36 |
37 |
42 |
{children}
43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Accordion; 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/PublicToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { getImagePrivacy, updateImagePrivacy } from "../../lib/image"; 3 | import Loader from "../../assets/Loader"; 4 | 5 | export default function PublicToggle({ imageId }: { imageId: string }) { 6 | const [isPublic, setIsPublic] = useState(null); 7 | useEffect(() => { 8 | getImagePrivacy(imageId).then(setIsPublic); 9 | }, [imageId]); 10 | 11 | if (isPublic === null) { 12 | return ; 13 | } 14 | 15 | async function handleChangeImagePublic(isPublic: boolean) { 16 | setIsPublic(null); 17 | updateImagePrivacy(imageId, isPublic) 18 | .then(() => { 19 | setIsPublic(isPublic); 20 | }) 21 | .catch(() => { 22 | setIsPublic(false); 23 | }); 24 | } 25 | 26 | return ( 27 | <> 28 | { 33 | handleChangeImagePublic(e.target.checked); 34 | }} 35 | /> 36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /client/src/assets/Close.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export function IconCloseCircle({ 4 | onClick, 5 | className, 6 | }: { 7 | onClick?: () => void; 8 | className?: string; 9 | }) { 10 | const [hover, setHover] = useState(false); 11 | return ( 12 | setHover(true)} 17 | onMouseLeave={() => setHover(false)} 18 | onClick={onClick} 19 | > 20 | {hover ? ( 21 | 22 | ) : ( 23 | <> 24 | {" "} 25 | 26 | 27 | 28 | )} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/width.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Width({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/height.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Height({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/rotate.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Rotate({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FiltersEffect/blur.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Blur({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /server/src/multerConfig.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import multer from "multer"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { v4 as uuid } from "uuid"; 6 | 7 | const storage = multer.diskStorage({ 8 | destination: (req, file, cb) => { 9 | const filePath = path.join(__dirname, "..", "..", "images"); 10 | if (!fs.existsSync(filePath)) { 11 | fs.mkdirSync(filePath, { recursive: true }); 12 | } 13 | cb(null, filePath); 14 | }, 15 | filename: (req, file, cb) => { 16 | const fileName = 17 | file.originalname + "-" + uuid() + path.extname(file.originalname); 18 | cb(null, fileName); 19 | }, 20 | }); 21 | 22 | const fileFilter = (req: Request, file: Express.Multer.File, cb: Function) => { 23 | if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") { 24 | cb(null, true); 25 | } else { 26 | cb(new Error("Only .png, .jpg and .jpeg format allowed!"), false); 27 | } 28 | }; 29 | 30 | const upload = multer({ 31 | storage: storage, 32 | fileFilter: fileFilter, 33 | limits: { 34 | fileSize: 15 * 1024 * 1024, 35 | }, 36 | }); 37 | 38 | export async function handleMulterErrors( 39 | err: Error, 40 | req: Request, 41 | res: Response, 42 | next: NextFunction 43 | ) { 44 | console.log("multer error", err); 45 | 46 | if (err instanceof multer.MulterError) { 47 | return res.status(400).json({ message: err.message }); 48 | } else if (err) { 49 | return res.status(500).json({ message: err.message }); 50 | } 51 | next(); 52 | } 53 | 54 | export default upload; 55 | 56 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/fase.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Fade({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FiltersEffect/gaussian.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Gaussian({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FiltersEffect/pixelate.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Pixelate({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/DrawingText/circle.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Circle({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/FileOperations/quality.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Quality({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/contrast.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Contrast({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/index.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from "../Accordian"; 2 | import { FiltersType } from "../base"; 3 | import Brightness from "./brightness"; 4 | import Contrast from "./contrast"; 5 | import Dither565 from "./dither565"; 6 | import Fade from "./fase"; 7 | import Greyscale from "./greyscale"; 8 | import Invert from "./invert"; 9 | import Normalize from "./normalize"; 10 | import Posterize from "./posterize"; 11 | import Sepia from "./sepia"; 12 | 13 | export default function ColorAdjustments({ 14 | title, 15 | changeAccordionState, 16 | currTitle, 17 | filterIndex, 18 | filters, 19 | setFilters, 20 | }: { 21 | title: string; 22 | changeAccordionState: (title: string) => void; 23 | currTitle: string; 24 | filterIndex: number; 25 | filters: FiltersType; 26 | setFilters: React.Dispatch>; 27 | }) { 28 | return ( 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/flip.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Flip({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/brightness.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Brightness({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/sepia.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Sepia({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /server/src/lib/pass.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { 3 | HASH_PREFIX, 4 | HASH_SALT, 5 | HASH_SUFFIX, 6 | JWT_SECRET, 7 | } from "../lib/constants"; 8 | import { TokenType } from "../schema/user"; 9 | 10 | const hashPass = (pass: string): string => { 11 | const saltedChars = pass 12 | .split("") 13 | .map((char) => `${HASH_SALT}${char}${HASH_SALT}`); 14 | const hashedPass = HASH_PREFIX + saltedChars.join("") + HASH_SUFFIX; 15 | return hashedPass; 16 | }; 17 | 18 | const unHashPass = (hashedPass: string): string => { 19 | const midHashedPass = hashedPass.slice( 20 | HASH_PREFIX.length, 21 | -HASH_SUFFIX.length 22 | ); 23 | const pass = midHashedPass.replace(new RegExp(HASH_SALT, "g"), ""); 24 | return pass; 25 | }; 26 | 27 | export const getToken = (password: string, username: string) => { 28 | const hashedPass = hashPass(password); 29 | return jwt.sign( 30 | { 31 | username, 32 | password: hashedPass, 33 | }, 34 | JWT_SECRET, 35 | { 36 | expiresIn: "7d", 37 | } 38 | ); 39 | }; 40 | 41 | export const decodeToken = (token: string): TokenType => { 42 | const decodeToken = jwt.verify(token, JWT_SECRET); 43 | const res = decodeToken as TokenType; 44 | if (!res.username || !res.password) { 45 | throw new Error("Invalid Token"); 46 | } 47 | return { username: res.username, password: res.password }; 48 | }; 49 | 50 | export const checkToken = (userPassword: string, dbPassword: string) => { 51 | const unHashedPass1 = unHashPass(dbPassword); 52 | const unHashedPass2 = unHashPass(userPassword); 53 | return unHashedPass1 === unHashedPass2; 54 | }; 55 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/xflip.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Xflip({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/yflip.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Yflip({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/invert.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Invert({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/dither565.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Dither565({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/greyscale.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Greyscale({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/normalize.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Normalize({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/EditImage/ColorAdjustments/posterize.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { FiltersType } from "../base"; 3 | 4 | export default function Posterize({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /server/src/schema/image.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export interface ImageManipulation { 4 | property: string; 5 | value: any; 6 | } 7 | 8 | export const checkNumStr = z.string().transform((val) => { 9 | const parsed = parseFloat(val); 10 | if (isNaN(parsed)) { 11 | return null; 12 | } 13 | return parsed; 14 | }); 15 | export const checkArrStr = z.string().transform((val) => { 16 | const parsed = val.split(",").map(Number); 17 | if (parsed.length !== 4) { 18 | return null; 19 | } 20 | return parsed; 21 | }); 22 | export const checkFlip = checkNumStr.transform((val) => { 23 | if (val === null) { 24 | return null; 25 | } 26 | if (![0, 1].includes(val)) { 27 | return null; 28 | } 29 | return val; 30 | }); 31 | export const checkValbetweenminus1and1 = checkNumStr.transform((val) => { 32 | if (val === null) { 33 | return null; 34 | } 35 | if (val < -1 || val > 1) { 36 | return null; 37 | } 38 | return val; 39 | }); 40 | export const checkValbetween0and1 = checkNumStr.transform((val) => { 41 | if (val === null) { 42 | return null; 43 | } 44 | if (val <= 0 || val >= 1) { 45 | return null; 46 | } 47 | return val; 48 | }); 49 | export const checkValbetween0and100 = checkNumStr.transform((val) => { 50 | if (val === null) { 51 | return null; 52 | } 53 | if (val < 0 || val >= 100) { 54 | return null; 55 | } 56 | return val; 57 | }); 58 | export const greaterThan0 = z.string().transform((val) => { 59 | if (val === null) { 60 | return null; 61 | } 62 | if (parseInt(val) > 0) { 63 | return parseInt(val); 64 | } 65 | return null; 66 | }); 67 | export const checkIfPresent = z.any().transform(() => true); 68 | -------------------------------------------------------------------------------- /client/src/assets/Copy.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function Copy({ 4 | className, 5 | onCopy, 6 | }: { 7 | className?: string; 8 | onCopy: () => void; 9 | }) { 10 | const [hovered, setHovered] = useState(false); 11 | return ( 12 | setHovered(true)} 17 | onMouseLeave={() => setHovered(false)} 18 | onClick={onCopy} 19 | > 20 | {hovered ? ( 21 | 22 | ) : ( 23 | 24 | )} 25 | 26 | ); 27 | } 28 | 29 | export function IconOpenInNew({ 30 | className, 31 | onClick, 32 | }: { 33 | className?: string; 34 | onClick: () => void; 35 | }) { 36 | return ( 37 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/EditImage/DraggableImage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | makeMoveable, 4 | DraggableProps, 5 | ScalableProps, 6 | RotatableProps, 7 | Rotatable, 8 | Draggable, 9 | Scalable, 10 | } from "react-moveable"; 11 | import MoveableHelper from "moveable-helper"; 12 | import { flushSync } from "react-dom"; 13 | import Loader from "../../assets/Loader"; 14 | 15 | const Moveable = makeMoveable([ 16 | Draggable, 17 | Scalable, 18 | Rotatable, 19 | ]); 20 | 21 | export default function TreeShakingApp({ 22 | src, 23 | className, 24 | loading, 25 | }: { 26 | src: string; 27 | className: string; 28 | loading: boolean; 29 | }) { 30 | const [helper] = React.useState(() => { 31 | return new MoveableHelper(); 32 | }); 33 | const targetRef = React.useRef(null); 34 | return ( 35 |
36 |
37 | {src ? ( 38 | { 43 | console.log(e); 44 | }} 45 | /> 46 | ) : ( 47 |
48 | 49 |
50 | )} 51 |
52 | 53 | 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /client/src/components/Login/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import AuthGraphic from "../../assets/AuthGraphic"; 3 | import Alert from "../../assets/Alert"; 4 | import { useRecoilState } from "recoil"; 5 | import { tokenState } from "../../state/token"; 6 | 7 | export default function Layout({ formUI }: { formUI: JSX.Element }) { 8 | const [token, setToken] = useRecoilState(tokenState); 9 | function logOut() { 10 | setToken(undefined); 11 | localStorage.removeItem("token"); 12 | } 13 | return ( 14 |
15 |
16 | 17 |
18 | {token && ( 19 |
20 | 21 | You are already logged in, go to dashboard 22 | 23 | 26 |
27 | )} 28 |
33 |
34 |

35 | Log in 36 |

37 | {formUI} 38 |
39 |

40 | Already have an account?{" "} 41 | 42 | Register here 43 | 44 |

45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /client/src/components/Signup/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import AuthGraphic from "../../assets/AuthGraphic"; 3 | import Alert from "../../assets/Alert"; 4 | import { useRecoilState } from "recoil"; 5 | import { tokenState } from "../../state/token"; 6 | 7 | export default function Layout({ formUI }: { formUI: JSX.Element }) { 8 | const [token, setToken] = useRecoilState(tokenState); 9 | function logOut() { 10 | setToken(undefined); 11 | localStorage.removeItem("token"); 12 | } 13 | return ( 14 |
15 |
16 | 17 |
18 | {token && ( 19 |
20 | 21 | You are already logged in, go to dashboard 22 | 23 | 26 |
27 | )} 28 |
33 |
34 |

35 | Sign Up 36 |

37 | {formUI} 38 |
39 |

40 | Already have an account?{" "} 41 | 42 | Register here 43 | 44 |

45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /client/src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink } from "react-router-dom"; 2 | import React from "react"; 3 | import { Popover, PopoverHandler } from "@material-tailwind/react"; 4 | import { User } from "./User"; 5 | import { useRecoilValue } from "recoil"; 6 | import { tokenState } from "../../state/token"; 7 | 8 | const Navbar = () => { 9 | const token = useRecoilValue(tokenState); 10 | const [openPopover, setOpenPopover] = React.useState(false); 11 | 12 | const triggers = { 13 | onMouseEnter: () => setOpenPopover(true), 14 | onMouseLeave: () => setOpenPopover(false), 15 | }; 16 | return ( 17 | 46 | ); 47 | }; 48 | 49 | export default Navbar; 50 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import Images from "./Images"; 2 | import { Link } from "react-router-dom"; 3 | import { useEffect, useState } from "react"; 4 | import { useRecoilValue } from "recoil"; 5 | import { tokenState } from "../../state/token"; 6 | 7 | export default function Dashboard() { 8 | const token = useRecoilValue(tokenState); 9 | const [navHeight, setNavHeight] = useState(0); 10 | useEffect(() => { 11 | setNavHeight(document.getElementsByTagName("nav")[0].clientHeight); 12 | }, []); 13 | return ( 14 |
20 | {token ? ( 21 | <> 22 |
23 | 28 | 29 | 30 | 31 | Right click on an image to edit 32 |
33 | 34 | 35 | ) : ( 36 |
37 |
38 |
39 | 40 | 43 | 44 | 45 | 48 | 49 |
50 |
51 |
52 | )} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /client/src/assets/Speedial.tsx: -------------------------------------------------------------------------------- 1 | export function IconBxHomeCircle() { 2 | return ( 3 | 4 | 5 | 6 | 7 | ); 8 | } 9 | export function IconDashboardLine() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export function IconUpload() { 19 | return ( 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | export function IconLogout() { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /client/src/lib/image.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { validateImageFetch } from "../schem/image"; 3 | import { BACKEND_URL } from "./constants"; 4 | 5 | export async function getAllImages() { 6 | const resp = await axios.get(BACKEND_URL + "/api/image", { 7 | headers: { 8 | Authorization: `Bearer ${localStorage.getItem("token")}`, 9 | }, 10 | }); 11 | const parsedResp = validateImageFetch.safeParse(resp.data); 12 | if (!parsedResp.success) { 13 | return []; 14 | } 15 | return parsedResp.data; 16 | } 17 | 18 | export async function getImage(id: string) { 19 | const resp = await axios.get(BACKEND_URL + `/api/image/${id}`, { 20 | headers: { 21 | Authorization: `Bearer ${localStorage.getItem("token")}`, 22 | }, 23 | responseType: "blob", 24 | }); 25 | return URL.createObjectURL(resp.data); 26 | } 27 | 28 | export async function getImageDetails(id: string) { 29 | const resp = await axios.get(BACKEND_URL + `/api/image/${id}/details`, { 30 | headers: { 31 | Authorization: `Bearer ${localStorage.getItem("token")}`, 32 | }, 33 | }); 34 | return resp.data; 35 | } 36 | 37 | export function uploadImage(file: File) { 38 | const formData = new FormData(); 39 | formData.append("image", file); 40 | return axios.post(BACKEND_URL + "/api/image", formData, { 41 | headers: { 42 | Authorization: `Bearer ${localStorage.getItem("token")}`, 43 | }, 44 | }); 45 | } 46 | 47 | export function deleteImage(id: string) { 48 | return axios.delete(BACKEND_URL + `/api/image/${id}`, { 49 | headers: { 50 | Authorization: `Bearer ${localStorage.getItem("token")}`, 51 | }, 52 | }); 53 | } 54 | 55 | export async function getImagePrivacy(id: string) { 56 | const res = await axios.get(BACKEND_URL + `/api/image/privacy/${id}`, { 57 | headers: { 58 | Authorization: `Bearer ${localStorage.getItem("token")}`, 59 | }, 60 | }); 61 | return res.data.isPublic === true; 62 | } 63 | 64 | export function updateImagePrivacy(id: string, isPublic: boolean) { 65 | return axios.patch( 66 | BACKEND_URL + `/api/image/privacy/${id}`, 67 | { isPublic }, 68 | { 69 | headers: { 70 | Authorization: `Bearer ${localStorage.getItem("token")}`, 71 | }, 72 | } 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /server/src/constants/image.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkNumStr, 3 | checkArrStr, 4 | checkFlip, 5 | checkIfPresent, 6 | checkValbetween0and1, 7 | checkValbetweenminus1and1, 8 | greaterThan0, 9 | checkValbetween0and100, 10 | } from "../schema/image"; 11 | 12 | export const sortQueryParamnsIndex = [ 13 | // Basic image manipulation 14 | { property: "rotate", type: checkNumStr }, // url: /image?rotate=90 0-360 15 | { property: "width", type: checkNumStr }, // url: /image?width=100 any number 16 | { property: "height", type: checkNumStr }, // url: /image?height=100 any number 17 | { property: "crop", type: checkArrStr }, // url: /image?crop=100,100,100,100 18 | { property: "flip", type: checkFlip }, // url: /image?flip=1 0 or 1 19 | { property: "xflip", type: checkFlip }, // url: /image?xflip=1 0 or 1 20 | { property: "yflip", type: checkFlip }, // url: /image?yflip=1 0 or 1 21 | 22 | // Color Adjustments 23 | { property: "brightness", type: checkValbetweenminus1and1 }, // url: /image?brightness=0.5 (-1)-1 24 | { property: "contrast", type: checkValbetweenminus1and1 }, // url: /image?contrast=0.5 (-1)-1 25 | { property: "dither565", type: checkIfPresent }, // url: /image?dither565 any 26 | { property: "greyscale", type: checkIfPresent }, // url: /image?greyscale any 27 | { property: "invert", type: checkIfPresent }, // url: /image?invert any 28 | { property: "normalize", type: checkIfPresent }, // url: /image?normalize any 29 | { property: "posterize", type: checkNumStr }, // url: /image?posterize=0.5 any number 30 | { property: "sepia", type: checkIfPresent }, // url: /image?sepia any 31 | { property: "fade", type: checkValbetween0and1 }, // url: /image?fade=0.5 32 | 33 | // Filters and Effects 34 | { property: "blur", type: greaterThan0 }, // url: /image?blur=2 any number greater than 0 35 | { property: "gaussian", type: greaterThan0 }, // url: /image?gaussian=0.5 any number greater than 0 36 | { property: "pixelate", type: checkNumStr }, // url: /image?pixelate=0.5 any number 37 | 38 | // Drawing and Text 39 | { property: "circle", type: checkIfPresent }, // url: /image?circle any 40 | 41 | // File Operations 42 | { property: "quality", type: checkValbetween0and100 }, // url: /image?quality=0.5 43 | ]; 44 | -------------------------------------------------------------------------------- /client/src/components/EditImage/BasicImageManipulation/crop.tsx: -------------------------------------------------------------------------------- 1 | import { IconCloseCircle } from "../../../assets/Close"; 2 | import { baseFilters, FiltersType } from "../base"; 3 | 4 | export default function Crop({ 5 | setFilters, 6 | filters, 7 | }: { 8 | setFilters: React.Dispatch>; 9 | filters: FiltersType; 10 | }) { 11 | return ( 12 |
13 |

Crop

14 |
15 | {[ 16 | Object.keys(baseFilters.crop).map((key, index) => ( 17 | 55 | )), 56 | ]} 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /server/src/constants/Image/aws.ts: -------------------------------------------------------------------------------- 1 | import aws from "aws-sdk"; 2 | import Image from "./index"; 3 | import Jimp from "jimp"; 4 | import { 5 | AWS_ACCESS_KEY_ID, 6 | AWS_REGION, 7 | AWS_S3_BUCKET, 8 | AWS_SECRET_ACCESS_KEY, 9 | } from "../../lib/constants"; 10 | 11 | export class AwsImage implements Image { 12 | private s3Client: aws.S3; 13 | public imageId: string; 14 | private config: { 15 | AWS_ACCESS_KEY_ID: string; 16 | AWS_SECRET_ACCESS_KEY: string; 17 | AWS_REGION: string; 18 | IMAGE_KEY?: string | undefined; 19 | }; 20 | 21 | constructor(IMAGE_KEY: string | undefined = undefined) { 22 | this.imageId = ""; 23 | this.config = { 24 | AWS_ACCESS_KEY_ID: AWS_ACCESS_KEY_ID, 25 | AWS_SECRET_ACCESS_KEY: AWS_SECRET_ACCESS_KEY, 26 | AWS_REGION: AWS_REGION, 27 | IMAGE_KEY: IMAGE_KEY, 28 | }; 29 | this.s3Client = new aws.S3({ 30 | credentials: { 31 | accessKeyId: AWS_ACCESS_KEY_ID, 32 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 33 | }, 34 | region: AWS_REGION, 35 | }); 36 | } 37 | 38 | async uploadImage({ 39 | dbId, 40 | image, 41 | code, 42 | }: { 43 | image: Jimp | Buffer; 44 | dbId: string; 45 | code?: string; 46 | }): Promise { 47 | if (!this.config.IMAGE_KEY) { 48 | if (code) { 49 | const key = `filtered/${dbId}-${code}.png`; 50 | this.config.IMAGE_KEY = key; 51 | } else { 52 | const key = `original/${dbId}.png`; 53 | this.config.IMAGE_KEY = key; 54 | } 55 | } 56 | let buffer = 57 | image instanceof Buffer 58 | ? image 59 | : await image.getBufferAsync(Jimp.MIME_PNG); 60 | const params = { 61 | Bucket: AWS_S3_BUCKET, 62 | Body: buffer, 63 | ContentType: Jimp.MIME_PNG, 64 | Key: this.config.IMAGE_KEY, 65 | }; 66 | return await this.s3Client.putObject(params).promise(); 67 | } 68 | 69 | async deleteImage(): Promise { 70 | if (!this.config.IMAGE_KEY) { 71 | return; 72 | } 73 | const params = { 74 | Bucket: AWS_S3_BUCKET, 75 | Key: this.config.IMAGE_KEY, 76 | }; 77 | 78 | await this.s3Client.deleteObject(params).promise(); 79 | } 80 | 81 | async downloadImage() { 82 | if (!this.config.IMAGE_KEY) { 83 | return; 84 | } 85 | const params = { 86 | Bucket: AWS_S3_BUCKET, 87 | Key: this.config.IMAGE_KEY, 88 | }; 89 | return await this.s3Client.getObject(params).promise(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /server/src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import prisma from "../client"; 3 | import { UserCreateSchema, UserLoginSchema } from "../schema/user"; 4 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; 5 | import { getToken } from "../lib/pass"; 6 | 7 | export async function handleSignupUser(req: Request, resp: Response) { 8 | const user = UserCreateSchema.safeParse(req.body); 9 | 10 | if (!user.success) { 11 | return resp.status(400).send(user.error.errors); 12 | } 13 | 14 | try { 15 | const newUser = await prisma.user.create({ 16 | data: { 17 | username: user.data.username, 18 | email: user.data.email, 19 | password: user.data.password, 20 | }, 21 | }); 22 | resp.status(200).send(newUser); 23 | } catch (e) { 24 | if (e instanceof PrismaClientKnownRequestError) { 25 | if (e.code === "P2002") { 26 | return resp.status(400).send({ 27 | message: `Signup failed, user with this ${( 28 | e?.meta?.target as [] 29 | ).join(",")} already exists`, 30 | status: "failed", 31 | }); 32 | } 33 | } 34 | resp.status(500).send({ 35 | message: "Internal Server Error", 36 | status: "failed", 37 | }); 38 | } 39 | } 40 | 41 | export async function handleLoginUser(req: Request, resp: Response) { 42 | const user = UserLoginSchema.safeParse(req.body); 43 | 44 | if (!user.success) { 45 | return resp.status(400).send(user.error.errors); 46 | } 47 | 48 | try { 49 | const foundUser = await prisma.user.findFirst({ 50 | where: { 51 | username: user.data.username, 52 | password: user.data.password, 53 | }, 54 | }); 55 | if (!foundUser) { 56 | return resp.status(404).send({ 57 | message: "User not found", 58 | status: "failed", 59 | }); 60 | } 61 | resp.cookie("token", getToken(foundUser.password, foundUser.username), { 62 | sameSite: "none", 63 | secure: true, 64 | httpOnly: false, 65 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), 66 | path: "/", 67 | }); 68 | resp.status(200).send({ 69 | message: "Successfully logged in", 70 | status: "success", 71 | token: getToken(foundUser.password, foundUser.username), 72 | }); 73 | } catch (e) { 74 | console.log(e); 75 | return resp.status(500).send({ 76 | message: "Internal Server Error", 77 | status: "failed", 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Images.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { getAllImages } from "../../lib/image"; 3 | import { useRecoilState, useSetRecoilState } from "recoil"; 4 | import { imagesState, openUploadDialogState } from "../../state/image"; 5 | import Image from "./Image"; 6 | 7 | import { Gallery } from "react-photoswipe-gallery"; 8 | import "photoswipe/dist/photoswipe.css"; 9 | 10 | export default function Images() { 11 | const [loading, setLoading] = useState(true); 12 | const [images, setImages] = useRecoilState(imagesState); 13 | const setOpenUploadDialogState = useSetRecoilState(openUploadDialogState); 14 | useEffect(() => { 15 | getAllImages() 16 | .then((images) => { 17 | setImages(images); 18 | }) 19 | .finally(() => { 20 | setLoading(false); 21 | }); 22 | }, [setImages]); 23 | return ( 24 |
31 | {loading ? ( 32 | 37 | 38 | 39 | ) : ( 40 | 41 | {images.length ? ( 42 | images.map((image, index) => ) 43 | ) : ( 44 | <> 45 |

No images found

46 | 57 | 58 | )} 59 |
60 | )} 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /client/src/components/EditImage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import DraggableImage from "./DraggableImage"; 3 | import { Link, useParams } from "react-router-dom"; 4 | import EditAccordian from "./EditAccordian"; 5 | import GridBg from "../../assets/grid.png"; 6 | import { getImage, getImageDetails } from "../../lib/image"; 7 | import { useRecoilValue, useSetRecoilState } from "recoil"; 8 | import { tokenState } from "../../state/token"; 9 | import { imagesState } from "../../state/image"; 10 | 11 | export default function EditImage() { 12 | const { imageId } = useParams(); 13 | const [url, setUrl] = useState(""); 14 | const [navHeight, setNavHeight] = useState(0); 15 | const [loading, setLoading] = useState(false); 16 | const token = useRecoilValue(tokenState); 17 | const setImages = useSetRecoilState(imagesState); 18 | useEffect(() => { 19 | setNavHeight(document.getElementsByTagName("nav")[0].clientHeight); 20 | }, []); 21 | useEffect(() => { 22 | if (!imageId || !token) return; 23 | getImage(imageId).then((url) => setUrl(url)); 24 | getImageDetails(imageId).then((data) => { 25 | return setImages((prev) => { 26 | return [...prev, data]; 27 | }); 28 | }); 29 | }, [imageId, setImages, token]); 30 | if (imageId) 31 | return token ? ( 32 |
38 | 43 |
44 | 48 |
49 | 56 |
57 | ) : ( 58 |
59 |
60 |
61 | 62 | 65 | 66 | 67 | 70 | 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /client/src/lib/signupFormOptions.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { FormSchema } from "useeform"; 3 | 4 | const formSchemaZod = { 5 | username: z.string().min(1), 6 | email: z.string().email(), 7 | password: z.string().min(8), 8 | }; 9 | 10 | const Options: FormSchema = { 11 | name: "test", 12 | className: 13 | "inline-flex flex-col justify-center items-center space-y-4 w-full bg-transparent w-full", 14 | preventDefault: true, 15 | onSubmit: (_, values, errors, zodErrors) => { 16 | console.log({ 17 | values, 18 | errors, 19 | zodErrors, 20 | }); 21 | if (errors.length) return console.log("Form has errors", errors); 22 | if (zodErrors.length) return; 23 | }, 24 | children: [ 25 | { 26 | formElement: "label", 27 | name: "label-username", 28 | className: "block text-sm font-medium text-gray-700 w-full", 29 | value: "Username", 30 | children: { 31 | formElement: "input", 32 | type: "text", 33 | name: "username", 34 | className: 35 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 36 | zodValidation: formSchemaZod.username, 37 | }, 38 | }, 39 | { 40 | formElement: "label", 41 | name: "label-email", 42 | className: "block text-sm font-medium text-gray-700 w-full", 43 | value: "Email", 44 | children: { 45 | formElement: "input", 46 | type: "email", 47 | name: "email", 48 | className: 49 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 50 | zodValidation: formSchemaZod.email, 51 | }, 52 | }, 53 | { 54 | formElement: "label", 55 | name: "label-password", 56 | className: "block text-sm font-medium text-gray-700 w-full", 57 | value: "Password", 58 | children: { 59 | formElement: "input", 60 | type: "password", 61 | name: "password", 62 | className: 63 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 64 | zodValidation: formSchemaZod.password, 65 | }, 66 | }, 67 | { 68 | formElement: "button", 69 | type: "submit", 70 | name: "submit", 71 | className: 72 | "w-full bg-black text-white p-2 rounded-md hover:bg-gray-800 focus:outline-none focus:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-colors duration-300", 73 | value: "Sign Up", 74 | autoFocus: false, 75 | disabled: false, 76 | }, 77 | ], 78 | }; 79 | export default Options; 80 | -------------------------------------------------------------------------------- /server/src/constants/Image/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | import { 3 | CLOUDINARY_API_KEY, 4 | CLOUDINARY_API_SECRET, 5 | CLOUDINARY_CLOUD_NAME, 6 | } from "../../lib/constants"; 7 | import Image from "./index"; 8 | import Jimp from "jimp"; 9 | import streamifier from "streamifier"; 10 | 11 | console.log({ 12 | cloud_name: CLOUDINARY_CLOUD_NAME, 13 | api_key: CLOUDINARY_API_KEY, 14 | api_secret: CLOUDINARY_API_SECRET, 15 | }); 16 | 17 | cloudinary.config({ 18 | cloud_name: CLOUDINARY_CLOUD_NAME, 19 | api_key: CLOUDINARY_API_KEY, 20 | api_secret: CLOUDINARY_API_SECRET, 21 | }); 22 | 23 | export class CloudinaryImage implements Image { 24 | public imageId: string | undefined = undefined; 25 | constructor(imageId?: string) { 26 | this.imageId = imageId; 27 | } 28 | 29 | uploadImage({ 30 | code, 31 | dbId, 32 | image, 33 | }: { 34 | image: Jimp | Buffer; 35 | dbId?: string; 36 | code?: string; 37 | }) { 38 | return new Promise(async (resolve, reject) => { 39 | let cld_upload_stream = cloudinary.uploader.upload_stream( 40 | { 41 | public_id: this.imageId 42 | ? this.imageId 43 | : `${image instanceof Buffer ? "original" : "filtered"}/${dbId}${ 44 | code ? `-${code}` : "" 45 | }`, 46 | }, 47 | function (error, result) { 48 | if (error) { 49 | return reject(error); 50 | } 51 | resolve(result); 52 | } 53 | ); 54 | 55 | const imageBuffer = 56 | image instanceof Buffer 57 | ? image 58 | : await image.getBufferAsync(Jimp.MIME_PNG); 59 | 60 | streamifier 61 | .createReadStream(imageBuffer) 62 | .pipe(cld_upload_stream) 63 | .on("error", (error) => { 64 | reject(error); 65 | }); 66 | }); 67 | } 68 | 69 | async deleteImage(): Promise<{ 70 | result: string; 71 | }> { 72 | return new Promise((resolve, reject) => { 73 | const { imageId } = this; 74 | if (!imageId) return reject("No imageId"); 75 | cloudinary.uploader.destroy(imageId, function (error, result) { 76 | console.log({ error, result, imageId }); 77 | 78 | if (error) { 79 | return reject(error); 80 | } 81 | resolve({ 82 | result: "ok", 83 | }); 84 | }); 85 | }); 86 | } 87 | 88 | async downloadImage(): Promise { 89 | return new Promise((resolve, reject) => { 90 | if (!this.imageId) return reject("No imageId"); 91 | const imageUrl = cloudinary.url(this.imageId); 92 | fetch(imageUrl) 93 | .then((response) => { 94 | if (!response.ok) { 95 | throw new Error("Failed to download image"); 96 | } 97 | return response.arrayBuffer(); 98 | }) 99 | .then((buffer) => resolve(Buffer.from(buffer))) 100 | .catch((error) => reject(error)); 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /client/src/components/SpeedDial.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { IconBxHomeCircle, IconLogout, IconUpload } from "../assets/Speedial"; 4 | import { useSetRecoilState } from "recoil"; 5 | import { openUploadDialogState } from "../state/image"; 6 | import { tokenState } from "../state/token"; 7 | 8 | export default function DefaultSpeedDial() { 9 | const setCookies = useSetRecoilState(tokenState); 10 | const [show, setshow] = useState(false); 11 | const setOpenUploadDialogState = useSetRecoilState(openUploadDialogState); 12 | function toggleShow() { 13 | setshow(!show); 14 | } 15 | const items = [ 16 | { 17 | icon: , 18 | link: "/", 19 | title: "Home", 20 | }, 21 | { 22 | icon: , 23 | onClick: () => setOpenUploadDialogState(true), 24 | title: "Uplaod Image", 25 | }, 26 | { 27 | icon: , 28 | onClick: () => { 29 | setCookies(undefined); 30 | localStorage.removeItem("token"); 31 | }, 32 | title: "Logout", 33 | }, 34 | ]; 35 | return ( 36 |
setshow(false)} 39 | > 40 |
46 | {items.map((item, index) => { 47 | const props = { 48 | className: 49 | "flex justify-center items-center w-16 h-16 text-gray-900 hover:text-gray-900 bg-white rounded-full border border-gray-300 shadow-sm hover:bg-gray-50 focus:outline-none relative", 50 | title: item.title, 51 | }; 52 | return item.link ? ( 53 | 54 | {item.icon} 55 | 56 | ) : ( 57 | item.onClick && ( 58 | 61 | ) 62 | ); 63 | })} 64 |
65 | 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Image Optimization 2 | 3 | A comprehensive tool for optimizing images to improve performance and reduce file size without sacrificing quality. This project offers various features to enhance images for web and mobile applications. The best part is it works directly with url params like `(url-to-image)?width=1000` 4 | 5 | ## Features 6 | 7 | - **Compression**: Reduce image file sizes while maintaining visual quality. 8 | - **Resizing**: Adjust image dimensions to fit specific requirements. 9 | - **Format Conversion**: Convert images between different formats (e.g., PNG, JPEG, WebP) for better performance. 10 | - **Batch Processing**: Optimize multiple images at once. 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | 16 | - Node.js 17 | - npm or yarn 18 | 19 | ### Installation 20 | 21 | 1. Clone the repository: 22 | ```bash 23 | git clone https://github.com/Pulkitxm/image-tweaker.git 24 | cd image-tweaker 25 | 26 | // start the client 27 | cd client 28 | npm install 29 | npm run dev 30 | 31 | // start the server 32 | cd server 33 | npm install 34 | npm run dev 35 | ``` 36 | 37 | 2. Using docker (**Note:** Ensure Docker is installed and running on your machine before executing the `docker compose up` command. This will build and start the necessary containers for the application. Also the .env files must be added into client and server.) 38 | 39 | ```bash 40 | docker compose up 41 | ``` 42 | 43 | You can find the docker images directly at: [@pulkitxm/image-optimization-client](https://hub.docker.com/r/pulkitxm/image-optimization-client) and [@pulkitxm/image-optimization-server](https://hub.docker.com/r/pulkitxm/image-optimization-server) 44 | 45 | # Technologies Used 46 | 47 | This project leverages various technologies and tools to achieve efficient image optimization. Below is a list of the key technologies used: 48 | 49 | ## Programming Languages 50 | 51 | - **JavaScript**: Used for scripting and implementing the core functionalities of the image optimization tool. 52 | - **Node.js**: Provides the runtime environment for executing JavaScript code on the server side. 53 | 54 | ## Libraries and Frameworks 55 | 56 | - **FFmpeg**: A powerful multimedia framework used for handling image and video processing tasks. It helps in resizing, compressing, and converting images. 57 | - **ImageMagick**: A robust image manipulation tool used for performing a variety of operations on images, such as resizing and format conversion. 58 | 59 | ## Tools 60 | 61 | - **npm**: The package manager used for managing dependencies and running scripts in the project. 62 | - **yarn**: An alternative package manager to npm, providing faster installation and additional features. 63 | - **jimp**: A JavaScript image processing library for Node.js. It provides functionality for image manipulation tasks like resizing, cropping, and color adjustments. Jimp is lightweight and easy to use for various image operations. 64 | 65 | ## Development Tools 66 | 67 | - **Visual Studio Code**: An integrated development environment (IDE) used for coding and debugging. 68 | - **Git**: A version control system used for tracking changes in the project and collaborating with others. 69 | 70 | ## Hosting and Deployment 71 | 72 | - **GitHub**: Used for hosting the project repository and managing code versions. 73 | - **Docker**: Facilitates deployment by creating containerized environments that encapsulate the application and its dependencies. 74 | 75 | ## Other 76 | 77 | - **Markdown**: Used for writing documentation files such as `README.md` and `TECHNOLOGIES.md`. 78 | 79 | These technologies work together to provide a robust solution for optimizing images, enhancing performance, and ensuring high-quality results. 80 | -------------------------------------------------------------------------------- /client/src/components/DeleteImage/Delete.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import toast from "react-hot-toast"; 3 | import { SetterOrUpdater, useSetRecoilState } from "recoil"; 4 | import { deleteImage } from "../../lib/image"; 5 | import { imagesState } from "../../state/image"; 6 | 7 | interface DeleteConfig { 8 | open: boolean; 9 | imageId: string; 10 | imageUrl: string; 11 | } 12 | 13 | export default function Delete({ 14 | deleteImageConfig, 15 | setDeleteImageConfig, 16 | }: { 17 | deleteImageConfig: DeleteConfig; 18 | setDeleteImageConfig: SetterOrUpdater; 19 | }) { 20 | const setImages = useSetRecoilState(imagesState); 21 | useEffect(() => { 22 | function handleCloseWithEsc(e: KeyboardEvent) { 23 | if (e.key === "Escape") { 24 | setDeleteImageConfig({ open: false, imageId: "", imageUrl: "" }); 25 | } 26 | } 27 | window.addEventListener("keydown", handleCloseWithEsc); 28 | return () => window.removeEventListener("keydown", handleCloseWithEsc); 29 | }, [setDeleteImageConfig]); 30 | function closeModal() { 31 | setDeleteImageConfig({ open: false, imageId: "", imageUrl: "" }); 32 | } 33 | function handleDleteImage() { 34 | toast.promise(deleteImage(deleteImageConfig.imageId), { 35 | loading: "Deleting image...", 36 | success: () => { 37 | setImages((prevImages) => 38 | prevImages.filter((image) => image.id !== deleteImageConfig.imageId) 39 | ); 40 | closeModal(); 41 | return "Image deleted successfully"; 42 | }, 43 | error: (err) => { 44 | if (err.response.data.message) return err.response.data.message; 45 | return "Failed to delete image"; 46 | }, 47 | }); 48 | } 49 | 50 | return ( 51 |
52 |

53 | {" "} 66 | Are you sure you want to delete this image? 67 |

68 | delete e.preventDefault()} 73 | /> 74 |
75 | 83 | 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /server/src/db/user.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../client"; 2 | 3 | export async function getImagesFromDb(user_id: string, includAllUser: boolean) { 4 | const allImages: any[] = []; 5 | 6 | const images = await prisma.image.findMany({ 7 | where: { 8 | createdById: user_id, 9 | }, 10 | select: { 11 | id: true, 12 | isPublic: true, 13 | createdById: true, 14 | }, 15 | }); 16 | allImages.push(...images); 17 | 18 | if (includAllUser) { 19 | const allUsersImages = await prisma.image.findMany({ 20 | where: { 21 | isPublic: true, 22 | }, 23 | select: { 24 | id: true, 25 | isPublic: true, 26 | createdById: true, 27 | }, 28 | }); 29 | allImages.push( 30 | ...allUsersImages.filter( 31 | (image) => allImages.find((i) => i.id === image.id) === undefined 32 | ) 33 | ); 34 | } 35 | const allUniqueImages = Array.from(new Set(allImages)); 36 | return allUniqueImages; 37 | } 38 | 39 | export async function getNumOfImages(user_id: string) { 40 | const numOfImages = await prisma.image.count({ 41 | where: { 42 | createdById: user_id, 43 | }, 44 | }); 45 | return numOfImages; 46 | } 47 | 48 | export async function getAnyImageKeyById(public_id: string) { 49 | const dbImage = await prisma.image.findFirst({ 50 | where: { 51 | id: public_id, 52 | }, 53 | select: { 54 | id: true, 55 | imageKey: true, 56 | isPublic: true, 57 | createdById: true, 58 | }, 59 | }); 60 | if (!dbImage) { 61 | return { 62 | id: public_id, 63 | imageKey: undefined, 64 | isPublic: undefined, 65 | createdById: undefined, 66 | isOwner: false, 67 | }; 68 | } 69 | return { 70 | id: dbImage.id, 71 | imageKey: dbImage.imageKey, 72 | isPublic: dbImage.isPublic, 73 | createdById: dbImage.createdById, 74 | isOwner: false, 75 | }; 76 | } 77 | 78 | export async function getImageKeyById( 79 | public_id: string, 80 | user_id: string 81 | ): Promise { 82 | const dbImage = await prisma.image.findFirst({ 83 | where: { 84 | id: public_id, 85 | createdById: user_id, 86 | }, 87 | select: { 88 | imageKey: true, 89 | }, 90 | }); 91 | return dbImage?.imageKey; 92 | } 93 | 94 | export async function getImagePrivacyById( 95 | public_id: string, 96 | user_id: string 97 | ): Promise { 98 | const dbImage = await prisma.image.findFirst({ 99 | where: { 100 | id: public_id, 101 | createdById: user_id, 102 | }, 103 | select: { 104 | isPublic: true, 105 | }, 106 | }); 107 | return dbImage?.isPublic; 108 | } 109 | 110 | export async function addImageToDb( 111 | imageKey: string, 112 | user_id: string 113 | ): Promise<{ 114 | id: string; 115 | isPublic: boolean; 116 | }> { 117 | const dbImage = await prisma.image.create({ 118 | data: { 119 | imageKey, 120 | createdById: user_id, 121 | }, 122 | }); 123 | return { 124 | id: dbImage.id, 125 | isPublic: dbImage.isPublic, 126 | }; 127 | } 128 | 129 | export async function deleteImageFromDb( 130 | imageId: string, 131 | user_id: string 132 | ): Promise { 133 | const deletedImage = await prisma.image.delete({ 134 | where: { 135 | id: imageId, 136 | createdById: user_id, 137 | }, 138 | }); 139 | return deletedImage.id; 140 | } 141 | 142 | export async function updateImagePrivacyToDb( 143 | imageId: string, 144 | isPublic: boolean, 145 | user_id: string 146 | ): Promise { 147 | const updatedImage = await prisma.image.update({ 148 | where: { 149 | id: imageId, 150 | createdById: user_id, 151 | }, 152 | data: { 153 | isPublic, 154 | }, 155 | }); 156 | return updatedImage.id; 157 | } 158 | -------------------------------------------------------------------------------- /server/src/utils/image.ts: -------------------------------------------------------------------------------- 1 | import Jimp from "jimp"; 2 | import QueryString from "qs"; 3 | import { ImageManipulation } from "../schema/image"; 4 | import { sortQueryParamnsIndex } from "../constants/image"; 5 | 6 | export function sortQueryParamns(searchParams: QueryString.ParsedQs) { 7 | let sortedParams: ImageManipulation[] = Array.from({ 8 | length: sortQueryParamnsIndex.length, 9 | }).fill({ property: "", value: null }) as ImageManipulation[]; 10 | Object.keys(searchParams) 11 | .sort() 12 | .forEach((key) => { 13 | if (sortQueryParamnsIndex.some((prop) => prop.property === key)) { 14 | const propIndex = sortQueryParamnsIndex.findIndex( 15 | (prop) => prop.property === key 16 | ); 17 | const obj = sortQueryParamnsIndex[propIndex].type.safeParse( 18 | searchParams[key] 19 | ); 20 | if (obj.success && obj.data != null) 21 | sortedParams[propIndex] = { property: key, value: obj.data }; 22 | } 23 | }); 24 | sortedParams = sortedParams.filter((param) => param.value !== null); 25 | return { 26 | sortedParams, 27 | code: getCode(sortedParams), 28 | }; 29 | } 30 | 31 | export function getCode(sortedParams: ImageManipulation[]) { 32 | let code = ""; 33 | sortedParams.forEach((param) => { 34 | const propIndex = sortQueryParamnsIndex.findIndex( 35 | (prop) => prop.property === param.property 36 | ); 37 | code += `${propIndex}(${param.value})`; 38 | }); 39 | return code; 40 | } 41 | 42 | export function manipulateImage( 43 | img: Jimp, 44 | sortedParams: ImageManipulation[] 45 | ): Jimp { 46 | let { width, height } = img.bitmap; 47 | sortedParams.forEach((param) => { 48 | switch (param.property) { 49 | case "rotate": 50 | img.rotate(param.value); 51 | break; 52 | case "width": 53 | width = param.value; 54 | break; 55 | case "height": 56 | height = param.value; 57 | break; 58 | case "crop": 59 | const [x, y, w, h] = param.value; 60 | img.crop(x, y, w, h); 61 | break; 62 | case "flip": 63 | img.flip(Boolean(param.value), Boolean(param.value)); 64 | break; 65 | case "xflip": 66 | img.flip(Boolean(param.value), false); 67 | break; 68 | case "yflip": 69 | img.flip(false, Boolean(param.value)); 70 | break; 71 | case "brightness": 72 | img.brightness(param.value); 73 | break; 74 | case "contrast": 75 | img.contrast(param.value); 76 | break; 77 | case "dither565": 78 | img.dither565(); 79 | break; 80 | case "greyscale": 81 | img.greyscale(); 82 | break; 83 | case "invert": 84 | img.invert(); 85 | break; 86 | case "normalize": 87 | img.normalize(); 88 | break; 89 | case "posterize": 90 | img.posterize(param.value); 91 | break; 92 | case "sepia": 93 | img.sepia(); 94 | break; 95 | case "fade": 96 | img.fade(param.value); 97 | break; 98 | case "blur": 99 | img.blur(param.value); 100 | break; 101 | case "gaussian": 102 | img.gaussian(param.value); 103 | break; 104 | case "pixelate": 105 | img.pixelate(param.value); 106 | break; 107 | case "convolution": 108 | img.convolution(param.value); 109 | break; 110 | case "circle": 111 | img.circle(); 112 | break; 113 | case "quality": 114 | img.quality(param.value); 115 | break; 116 | default: 117 | break; 118 | } 119 | }); 120 | if ( 121 | width && 122 | height && 123 | (width !== img.bitmap.width || height !== img.bitmap.height) 124 | ) { 125 | img.resize(width, height); 126 | } 127 | return img; 128 | } 129 | -------------------------------------------------------------------------------- /client/src/components/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import useForm from "useeform"; 2 | import axios from "axios"; 3 | import { z } from "zod"; 4 | import Loader from "../../assets/Loader"; 5 | import Layout from "./Layout"; 6 | import { useNavigate } from "react-router-dom"; 7 | import toast from "react-hot-toast"; 8 | import { BACKEND_URL } from "../../lib/constants"; 9 | import { useSetRecoilState } from "recoil"; 10 | import { tokenState } from "../../state/token"; 11 | 12 | export default function Login() { 13 | const navigate = useNavigate(); 14 | const setToken = useSetRecoilState(tokenState); 15 | const formSchemaZod = { 16 | username: z.string().min(1), 17 | password: z.string().min(8), 18 | }; 19 | const { formUI } = useForm({ 20 | name: "test", 21 | className: 22 | "inline-flex flex-col justify-center items-center space-y-4 w-full bg-transparent w-full", 23 | onSubmit: async (event, values, errors, zodErrors) => { 24 | event.preventDefault(); 25 | 26 | const { username, password } = values; 27 | 28 | if ((password as string).length < 8) 29 | return toast.error("Password must be at least 8 characters long"); 30 | if ((username as string).length <= 1) 31 | return toast.error("Username must be at least 1 character long"); 32 | 33 | if (zodErrors.length != 0 || errors.length != 0) 34 | return toast.error("Invalid credentials"); 35 | 36 | toast.promise( 37 | axios.post(BACKEND_URL + "/api/auth/login", values).then((res) => { 38 | const token = res.data.token; 39 | localStorage.setItem("token", token); 40 | setToken(token); 41 | }), 42 | { 43 | loading: "Logging in...", 44 | success: () => { 45 | navigate("/"); 46 | return "Logged in successfully"; 47 | }, 48 | error: (err) => { 49 | if (err.response.data.message) return err.response.data.message; 50 | return "Internal Server Error"; 51 | }, 52 | }, 53 | { 54 | success: { 55 | duration: 3000, 56 | }, 57 | error: { 58 | duration: 3000, 59 | }, 60 | } 61 | ); 62 | }, 63 | children: [ 64 | { 65 | formElement: "label", 66 | name: "label-username", 67 | className: "block text-sm font-medium text-gray-700 w-full", 68 | value: "Username", 69 | children: { 70 | formElement: "input", 71 | type: "text", 72 | name: "username", 73 | className: 74 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 75 | zodValidation: formSchemaZod.username, 76 | required: true, 77 | }, 78 | }, 79 | { 80 | formElement: "label", 81 | name: "label-password", 82 | className: "block text-sm font-medium text-gray-700 w-full", 83 | value: "Password", 84 | children: { 85 | formElement: "input", 86 | type: "password", 87 | name: "password", 88 | className: 89 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 90 | zodValidation: formSchemaZod.password, 91 | required: true, 92 | }, 93 | }, 94 | { 95 | formElement: "button", 96 | type: "submit", 97 | name: "submit", 98 | loading: true, 99 | loadingComponent: , 100 | className: 101 | "w-full bg-black text-white p-2 rounded-md hover:bg-gray-800", 102 | value: "Log In", 103 | autoFocus: false, 104 | disabled: false, 105 | }, 106 | ], 107 | }); 108 | return ; 109 | } 110 | -------------------------------------------------------------------------------- /client/src/assets/Image.tsx: -------------------------------------------------------------------------------- 1 | export function OpenInNewTab() { 2 | return ( 3 | 4 | 10 | 11 | ); 12 | } 13 | export function IconDownload() { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export function IconBxsFileJpg() { 23 | return ( 24 | 25 | 29 | 30 | ); 31 | } 32 | 33 | export function IconPng() { 34 | return ( 35 | 36 | 40 | 41 | ); 42 | } 43 | 44 | export function IconBxsEdit() { 45 | return ( 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | export function IconBxDelete() { 54 | return ( 55 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export function IconCardImage() { 66 | return ( 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /client/src/components/Signup/index.tsx: -------------------------------------------------------------------------------- 1 | import useForm from "useeform"; 2 | import axios from "axios"; 3 | import { z } from "zod"; 4 | import Loader from "../../assets/Loader"; 5 | import Layout from "./Layout"; 6 | import { useNavigate } from "react-router-dom"; 7 | import toast from "react-hot-toast"; 8 | import { BACKEND_URL } from "../../lib/constants"; 9 | 10 | export default function Login() { 11 | const navigate = useNavigate(); 12 | const formSchemaZod = { 13 | username: z.string().min(1), 14 | password: z.string().min(8), 15 | email: z.string().email(), 16 | }; 17 | const { formUI } = useForm({ 18 | name: "test", 19 | className: 20 | "inline-flex flex-col justify-center items-center space-y-4 w-full bg-transparent w-full", 21 | onSubmit: async (event, values, errors, zodErrors) => { 22 | event.preventDefault(); 23 | const { username, password, email } = values; 24 | 25 | if ((password as string).length < 8) 26 | return toast.error("Password must be at least 8 characters long"); 27 | if ((username as string).length <= 1) 28 | return toast.error("Username must be at least 1 character long"); 29 | if ((email as string).length <= 1) 30 | return toast.error("Email must be at least 1 character long"); 31 | 32 | if (zodErrors.length != 0 || errors.length != 0) 33 | return toast.error("Invalid credentials"); 34 | 35 | toast.promise( 36 | axios.post(BACKEND_URL + "/api/auth/register", values, { 37 | headers: { 38 | Authorization: `Bearer ${localStorage.getItem("token")}`, 39 | }, 40 | }), 41 | { 42 | loading: "Logging in...", 43 | success: () => { 44 | navigate("/login"); 45 | return "Logged in successfully"; 46 | }, 47 | error: (err) => { 48 | if (err.response.data.message) return err.response.data.message; 49 | return "Internal Server Error"; 50 | }, 51 | }, 52 | { 53 | success: { 54 | duration: 3000, 55 | }, 56 | error: { 57 | duration: 3000, 58 | }, 59 | } 60 | ); 61 | }, 62 | children: [ 63 | { 64 | formElement: "label", 65 | name: "label-username", 66 | className: "block text-sm font-medium text-gray-700 w-full", 67 | value: "Username", 68 | children: { 69 | formElement: "input", 70 | type: "text", 71 | name: "username", 72 | className: 73 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 74 | zodValidation: formSchemaZod.username, 75 | required: true, 76 | }, 77 | }, 78 | { 79 | formElement: "label", 80 | name: "label-email", 81 | className: "block text-sm font-medium text-gray-700 w-full", 82 | value: "Email", 83 | children: { 84 | formElement: "input", 85 | type: "text", 86 | name: "email", 87 | className: 88 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 89 | zodValidation: formSchemaZod.email, 90 | required: true, 91 | }, 92 | }, 93 | { 94 | formElement: "label", 95 | name: "label-password", 96 | className: "block text-sm font-medium text-gray-700 w-full", 97 | value: "Password", 98 | children: { 99 | formElement: "input", 100 | type: "password", 101 | name: "password", 102 | className: 103 | "mt-1 p-2 w-full border rounded-md focus:border-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors duration-300 w-full", 104 | zodValidation: formSchemaZod.password, 105 | required: true, 106 | }, 107 | }, 108 | { 109 | formElement: "button", 110 | type: "submit", 111 | name: "submit", 112 | loading: true, 113 | loadingComponent: , 114 | className: 115 | "w-full bg-black text-white p-2 rounded-md hover:bg-gray-800", 116 | value: "Log In", 117 | autoFocus: false, 118 | disabled: false, 119 | }, 120 | ], 121 | }); 122 | return ; 123 | } 124 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Image.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Menu, 3 | Item, 4 | Separator, 5 | Submenu, 6 | useContextMenu, 7 | } from "react-contexify"; 8 | import { useEffect, useState } from "react"; 9 | import { Image } from "../../schem/image"; 10 | import { getImage } from "../../lib/image"; 11 | import { 12 | IconBxDelete, 13 | IconBxsEdit, 14 | IconBxsFileJpg, 15 | IconDownload, 16 | IconPng, 17 | OpenInNewTab, 18 | } from "../../assets/Image"; 19 | import { useNavigate } from "react-router-dom"; 20 | import NotFound from "../../assets/NotFound.png"; 21 | import { useSetRecoilState } from "recoil"; 22 | import { deleteImageConfigState } from "../../state/image"; 23 | import { Item as ItemPSG } from "react-photoswipe-gallery"; 24 | import { BACKEND_URL } from "../../lib/constants"; 25 | 26 | export default function ImageCx({ image }: { image: Image }) { 27 | const [loading, setLoading] = useState(true); 28 | const [imageUrl, setImageUrl] = useState(""); 29 | const { show } = useContextMenu({ 30 | id: image.id, 31 | }); 32 | useEffect(() => { 33 | getImage(image.id) 34 | .then((res) => { 35 | setImageUrl(res); 36 | setLoading(false); 37 | }) 38 | .catch((err) => { 39 | console.error(err); 40 | setLoading(null); 41 | }); 42 | }, [image.id]); 43 | 44 | return ( 45 |
53 | {loading ? ( 54 |
55 | ) : loading === null ? ( 56 | Not Found e.preventDefault()} 62 | /> 63 | ) : ( 64 | <> 65 | 71 | {({ ref, open }) => ( 72 | Image { 81 | show({ 82 | event: e, 83 | }); 84 | }} 85 | /> 86 | )} 87 | 88 | 89 | 90 | )} 91 |
92 | ); 93 | } 94 | 95 | function MenuCX({ 96 | id, 97 | imageUrl, 98 | isOwner, 99 | }: { 100 | id: string; 101 | imageUrl: string; 102 | isOwner: boolean; 103 | }) { 104 | const navigate = useNavigate(); 105 | const setDeleteImageConfig = useSetRecoilState(deleteImageConfigState); 106 | function downlaodImage(type: "png" | "jpg") { 107 | const a = document.createElement("a"); 108 | a.href = imageUrl; 109 | a.download = "image." + type; 110 | a.click(); 111 | } 112 | return ( 113 | 118 | Id: {id} 119 | 120 | { 122 | window.open(BACKEND_URL + "/api/image/" + id, "_blank"); 123 | }} 124 | > 125 |   Open in new tab 126 | 127 | navigate("/edit/" + id)}> 128 |   Edit 129 | 130 | {isOwner && ( 131 | { 133 | setDeleteImageConfig({ 134 | open: true, 135 | imageId: id, 136 | imageUrl: imageUrl, 137 | }); 138 | }} 139 | > 140 |  

Delete

141 |
142 | )} 143 | 144 | 147 |   Download as 148 | 149 | } 150 | > 151 | downlaodImage("png")}> 152 |   PNG 153 | 154 | downlaodImage("jpg")}> 155 |   JPG 156 | 157 | 158 |
159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /client/src/components/EditImage/EditAccordian.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useState, useCallback, useMemo } from "react"; 3 | import toast from "react-hot-toast"; 4 | import BasicImageManipulation from "./BasicImageManipulation"; 5 | import { baseFilters, FiltersType } from "./base"; 6 | import ColorAdjustments from "./ColorAdjustments"; 7 | import FiltersEffects from "./FiltersEffect"; 8 | import FileOperations from "./FileOperations"; 9 | import DrawingText from "./DrawingText"; 10 | import Copy, { IconOpenInNew } from "../../assets/Copy"; 11 | import { BACKEND_URL } from "../../lib/constants"; 12 | import PublicToggle from "./PublicToggle"; 13 | import { useRecoilValue } from "recoil"; 14 | import { imagesState } from "../../state/image"; 15 | 16 | const EditAccordion = ({ 17 | loading, 18 | setLoading, 19 | setUrl, 20 | imageId, 21 | url, 22 | }: { 23 | setLoading: React.Dispatch>; 24 | setUrl: React.Dispatch>; 25 | loading: boolean; 26 | imageId: string; 27 | url: string; 28 | }) => { 29 | const images = useRecoilValue(imagesState); 30 | const image = useMemo(() => { 31 | return images.find((image) => image.id == imageId); 32 | }, [images, imageId]); 33 | const listOfFilters = [ 34 | { 35 | title: "Basic Image Manipulation", 36 | component: BasicImageManipulation, 37 | }, 38 | { 39 | title: "Colour Adjustments", 40 | component: ColorAdjustments, 41 | }, 42 | { 43 | title: "Filters & Effects", 44 | component: FiltersEffects, 45 | }, 46 | { 47 | title: "Drawing and Text", 48 | component: DrawingText, 49 | }, 50 | { 51 | title: "File Operations", 52 | component: FileOperations, 53 | }, 54 | ]; 55 | const [loadingFInalEdit, setLoadingFInalEdit] = useState(false); 56 | const [filters, setFilters] = useState(baseFilters); 57 | const finalUrl = useMemo(() => { 58 | let newFinalUrl = ""; 59 | 60 | Object.entries(filters).forEach(([key, value]) => { 61 | if ( 62 | key == "crop" && 63 | typeof value != "number" && 64 | Object.values(value).every((val) => val != -1) 65 | ) { 66 | const crop = value; 67 | newFinalUrl += `&crop=${crop.top},${crop.right},${crop.bottom},${crop.left}`; 68 | } else if (typeof value == "number" && value != -1) { 69 | newFinalUrl += `&${key}=${value}`; 70 | } 71 | }); 72 | return ( 73 | BACKEND_URL + 74 | "/api/image/" + 75 | imageId + 76 | (newFinalUrl ? "?" + newFinalUrl : "") 77 | ); 78 | }, [filters, imageId]); 79 | const [currTitle, setCurrTitle] = useState(""); 80 | const changeAccordionState = (title: string) => { 81 | setCurrTitle((prevTitle) => (prevTitle === title ? "" : title)); 82 | }; 83 | const handleApplyFilters = useCallback( 84 | async (reset = false) => { 85 | if (!url) { 86 | return toast.error( 87 | "Please wait for the image to load before applying filters" 88 | ); 89 | } 90 | if (loading || loadingFInalEdit) 91 | return toast.error( 92 | "Please wait for the previous operation to complete" 93 | ); 94 | if (!reset && !finalUrl) return toast.error("No filters selected"); 95 | setLoading(true); 96 | try { 97 | setLoadingFInalEdit(true); 98 | const respImage = await axios.get( 99 | reset 100 | ? `${BACKEND_URL}/api/image/${imageId}` 101 | : `${BACKEND_URL}/api/image/${imageId}?${finalUrl}`, 102 | { 103 | headers: { 104 | Authorization: `Bearer ${localStorage.getItem("token")}`, 105 | }, 106 | responseType: "blob", 107 | } 108 | ); 109 | const newUrl = URL.createObjectURL(respImage.data); 110 | setUrl(newUrl); 111 | reset && setFilters(baseFilters); 112 | } catch (error) { 113 | toast.error("Failed to apply filters"); 114 | } finally { 115 | setLoading(false); 116 | setLoadingFInalEdit(false); 117 | } 118 | }, 119 | [url, loading, loadingFInalEdit, finalUrl, setLoading, imageId, setUrl] 120 | ); 121 | 122 | return ( 123 |
124 |

Edit Image

125 |
126 |
127 |

{finalUrl}

128 |
129 |
130 | { 132 | navigator.clipboard.writeText(finalUrl); 133 | toast.success("Copied to clipboard"); 134 | }} 135 | /> 136 | window.open(finalUrl, "_blank")} 139 | /> 140 |
141 |
142 |
143 | 149 | 155 | {image && image.isOwner && ( 156 | 162 | )} 163 |
164 |
165 | {listOfFilters.map((item, filterIndex) => { 166 | const Component = item.component; 167 | return ( 168 | 177 | ); 178 | })} 179 |
180 | ); 181 | }; 182 | 183 | export default EditAccordion; 184 | -------------------------------------------------------------------------------- /client/src/components/Upload/UploadFile.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 2 | import toast from "react-hot-toast"; 3 | import { IconCloseCircle } from "../../assets/Close"; 4 | import { SetterOrUpdater, useSetRecoilState } from "recoil"; 5 | import { uploadImage } from "../../lib/image"; 6 | import { imagesState } from "../../state/image"; 7 | 8 | export default function UploadFile({ 9 | open, 10 | setOpen, 11 | }: { 12 | open: boolean; 13 | setOpen: SetterOrUpdater; 14 | }) { 15 | const dropZoneRef = useRef(null); 16 | const inputRef = useRef(null); 17 | const previewImgRef = useRef(null); 18 | const [file, setfile] = useState(null); 19 | const setImages = useSetRecoilState(imagesState); 20 | 21 | const fileSize = useMemo(() => { 22 | if (file) { 23 | const sizeInBytes = file.size; 24 | const formatFileSize = (size: number) => { 25 | const units = ["B", "KB", "MB"]; 26 | let index = 0; 27 | while (size >= 1024 && index < units.length - 1) { 28 | size /= 1024; 29 | index++; 30 | } 31 | return `${size.toFixed(2)} ${units[index]}`; 32 | }; 33 | return formatFileSize(sizeInBytes); 34 | } 35 | return null; 36 | }, [file]); 37 | 38 | useEffect(() => { 39 | const dropzone = dropZoneRef.current; 40 | const input = inputRef.current; 41 | const preview = previewImgRef.current; 42 | if (!dropzone || !input || !preview) return; 43 | 44 | dropzone.addEventListener("dragover", (e) => { 45 | e.preventDefault(); 46 | dropzone.classList.add("border-indigo-600"); 47 | }); 48 | 49 | dropzone.addEventListener("dragleave", (e) => { 50 | e.preventDefault(); 51 | dropzone.classList.remove("border-indigo-600"); 52 | }); 53 | 54 | function checkFileAndAdd(addfile: File) { 55 | const allowedTypes = ["image/jpeg", "image/png", "image/jpg"]; 56 | if (addfile && allowedTypes.includes(addfile.type)) { 57 | if (addfile.size <= 15 * 1024 * 1024) displayPreview(addfile); 58 | else toast.error("File size is too large"); 59 | } else { 60 | console.error("Invalid file type"); 61 | toast.error("Invalid file type"); 62 | } 63 | } 64 | 65 | dropzone.addEventListener("drop", (e) => { 66 | e.preventDefault(); 67 | dropzone.classList.remove("border-indigo-600"); 68 | const file = e.dataTransfer?.files[0]; 69 | file && checkFileAndAdd(file); 70 | }); 71 | 72 | input.addEventListener("change", (e) => { 73 | const target = e.target as HTMLInputElement; 74 | const file = target.files?.[0]; 75 | file && checkFileAndAdd(file); 76 | }); 77 | 78 | function displayPreview(file: File) { 79 | setfile(file); 80 | const reader = new FileReader(); 81 | reader.onload = () => { 82 | if (preview && typeof reader.result === "string") { 83 | preview.src = reader.result; 84 | preview.classList.remove("hidden"); 85 | } 86 | }; 87 | reader.readAsDataURL(file); 88 | } 89 | const handlePaste = (e: ClipboardEvent) => { 90 | const clipboardItems = e.clipboardData?.items; 91 | if (clipboardItems) { 92 | for (const item of clipboardItems) { 93 | if (item.type.startsWith("image/")) { 94 | const file = item.getAsFile(); 95 | file && checkFileAndAdd(file); 96 | } 97 | } 98 | } 99 | }; 100 | 101 | window.addEventListener("paste", handlePaste); 102 | 103 | return () => { 104 | window.removeEventListener("paste", handlePaste); 105 | }; 106 | }, []); 107 | useEffect(() => { 108 | window.addEventListener("keydown", (e) => { 109 | if (e.key === "Escape" && open) setOpen(false); 110 | }); 111 | }, [open, setOpen]); 112 | 113 | const handleUploadImage = useCallback(async () => { 114 | if (!file) return toast.error("No file selected"); 115 | toast.promise(uploadImage(file as File), { 116 | loading: "Uploading image...", 117 | success: (res) => { 118 | setImages((prev) => [ 119 | ...prev, 120 | { 121 | id: res.data.id, 122 | isPublic: res.data.isPublic, 123 | isOwner: true, 124 | }, 125 | ]); 126 | setOpen(false); 127 | return "Image uploaded successfully"; 128 | }, 129 | error: (err) => { 130 | if (err.response.data.message) return err.response.data.message; 131 | return "Failed to upload image"; 132 | }, 133 | }); 134 | }, [file, setImages, setOpen]); 135 | 136 | return ( 137 |
138 |
139 | { 141 | setOpen(false); 142 | }} 143 | /> 144 |
145 |
149 | 155 |
156 | 161 | 162 |
163 | 174 |
175 |

PNG, JPG up to 10MB

176 |
177 | 178 | 184 | {/* diplsay image size */} 185 | {fileSize && ( 186 |
187 |

{fileSize}

188 |
189 | )} 190 |
191 |
192 | 198 |
199 |
200 | ); 201 | } 202 | -------------------------------------------------------------------------------- /client/src/components/EditImage/base.ts: -------------------------------------------------------------------------------- 1 | const listOfFilters = [ 2 | { 3 | filter: "Basic image manipulation", 4 | children: [ 5 | { 6 | property: "rotate", 7 | vals: "single", 8 | validate: (val: string) => { 9 | const parsedVal = Number(val); 10 | return parsedVal >= 0 && parsedVal <= 360; 11 | }, 12 | error: "Please enter a number between 0 and 360", 13 | }, 14 | { 15 | property: "width", 16 | vals: "single", 17 | validate: (val: string) => { 18 | const parsedVal = Number(val); 19 | return parsedVal >= 0; 20 | }, 21 | error: "Please enter a number greater than 0", 22 | }, 23 | { 24 | property: "height", 25 | vals: "single", 26 | validate: (val: string) => { 27 | const parsedVal = Number(val); 28 | return parsedVal >= 0; 29 | }, 30 | error: "Please enter a number greater than 0", 31 | }, 32 | { 33 | property: "crop", 34 | vals: ["top", "right", "bottom", "left"], 35 | validate: (val: string) => { 36 | const parsedVal = val.split(","); 37 | return parsedVal.length === 4 && parsedVal.some((v) => Number(v)); 38 | }, 39 | error: "Please enter 4 numbers separated by commas", 40 | }, 41 | { 42 | property: "flip", 43 | vals: "single", 44 | validate: (val: string) => { 45 | const parsedVal = Number(val); 46 | return parsedVal === 0 || parsedVal === 1; 47 | }, 48 | error: "Please enter 0 or 1", 49 | }, 50 | { 51 | property: "xflip", 52 | vals: "single", 53 | validate: (val: string) => { 54 | const parsedVal = Number(val); 55 | return parsedVal === 0 || parsedVal === 1; 56 | }, 57 | error: "Please enter 0 or 1", 58 | }, 59 | { 60 | property: "yflip", 61 | vals: "single", 62 | validate: (val: string) => { 63 | const parsedVal = Number(val); 64 | return parsedVal === 0 || parsedVal === 1; 65 | }, 66 | error: "Please enter 0 or 1", 67 | }, 68 | ], 69 | }, 70 | { 71 | filter: "Color Adjustments", 72 | children: [ 73 | { 74 | property: "brightness", 75 | vals: "single", 76 | validate: (val: string) => { 77 | const parsedVal = Number(val); 78 | return parsedVal >= -1 && parsedVal <= 1; 79 | }, 80 | error: "Please enter a number between -1 and 1", 81 | }, 82 | { 83 | property: "contrast", 84 | vals: "single", 85 | validate: (val: string) => { 86 | const parsedVal = Number(val); 87 | return parsedVal >= -1 && parsedVal <= 1; 88 | }, 89 | error: "Please enter a number between -1 and 1", 90 | }, 91 | { 92 | property: "dither565", 93 | vals: "single", 94 | validate: (val: string) => (Number(val) ? true : false), 95 | error: "Please enter any value", 96 | }, 97 | { 98 | property: "greyscale", 99 | vals: "single", 100 | validate: (val: string) => (Number(val) ? true : false), 101 | error: "Please enter any value", 102 | }, 103 | { 104 | property: "invert", 105 | vals: "single", 106 | validate: (val: string) => (Number(val) ? true : false), 107 | error: "Please enter any value", 108 | }, 109 | { 110 | property: "normalize", 111 | vals: "single", 112 | validate: (val: string) => (Number(val) ? true : false), 113 | error: "Please enter any value", 114 | }, 115 | { 116 | property: "posterize", 117 | vals: "single", 118 | validate: (val: string) => (Number(val) ? true : false), 119 | error: "Please enter any value", 120 | }, 121 | { 122 | property: "sepia", 123 | vals: "single", 124 | validate: (val: string) => (Number(val) ? true : false), 125 | error: "Please enter any value", 126 | }, 127 | { 128 | property: "fade", 129 | vals: "single", 130 | validate: (val: string) => { 131 | const parsedVal = Number(val); 132 | return parsedVal >= 0 && parsedVal <= 1; 133 | }, 134 | error: "Please enter a number between 0 and 1", 135 | }, 136 | ], 137 | }, 138 | { 139 | filter: "Filters and Effects", 140 | children: [ 141 | { 142 | property: "blur", 143 | vals: "single", 144 | validate: (val: string) => (Number(val) > 0 ? true : false), 145 | error: "Please enter a number greater than 0", 146 | }, 147 | { 148 | property: "gaussian", 149 | vals: "single", 150 | validate: (val: string) => (Number(val) > 0 ? true : false), 151 | error: "Please enter a number greater than 0", 152 | }, 153 | { 154 | property: "pixelate", 155 | vals: "single", 156 | validate: (val: string) => (Number(val) ? true : false), 157 | error: "Please enter any value", 158 | }, 159 | ], 160 | }, 161 | { 162 | filter: "Drawing and Text", 163 | children: [ 164 | { 165 | property: "circle", 166 | vals: "single", 167 | validate: (val: string) => (Number(val) ? true : false), 168 | error: "Please enter any value", 169 | }, 170 | ], 171 | }, 172 | { 173 | filter: "File Operations", 174 | children: [ 175 | { 176 | property: "quality", 177 | vals: "single", 178 | validate: (val: string) => (Number(val) ? true : false), 179 | error: "Please enter any value", 180 | }, 181 | ], 182 | }, 183 | ]; 184 | export default listOfFilters; 185 | 186 | type val = number | null; 187 | interface filterValType { 188 | property: string; 189 | values: { 190 | [key: string]: val | val[]; 191 | }; 192 | } 193 | export function getDefaultValue() { 194 | const defaultValues: filterValType[] = []; 195 | listOfFilters.forEach((filter) => { 196 | const values: { 197 | [key: string]: val | val[]; 198 | } = {}; 199 | filter.children.forEach((child) => { 200 | if (Array.isArray(child.vals)) { 201 | values[child.property] = child.vals.map(() => null); 202 | } else { 203 | values[child.property] = null; 204 | } 205 | }); 206 | defaultValues.push({ 207 | property: filter.filter, 208 | values: values, 209 | }); 210 | }); 211 | return defaultValues; 212 | } 213 | 214 | export type FiltersType = { 215 | rotate: number; 216 | width: number; 217 | height: number; 218 | crop: { 219 | top: number; 220 | right: number; 221 | bottom: number; 222 | left: number; 223 | }; 224 | flip: number; 225 | xflip: number; 226 | yflip: number; 227 | brightness: number; 228 | contrast: number; 229 | dither565: number; 230 | greyscale: number; 231 | invert: number; 232 | normalize: number; 233 | posterize: number; 234 | sepia: number; 235 | fade: number; 236 | blur: number; 237 | gaussian: number; 238 | pixelate: number; 239 | circle: number; 240 | quality: number; 241 | }; 242 | 243 | 244 | export const baseFilters: FiltersType = { 245 | rotate: -1, 246 | width: -1, 247 | height: -1, 248 | crop: { 249 | top: -1, 250 | right: -1, 251 | bottom: -1, 252 | left: -1, 253 | }, 254 | flip: -1, 255 | xflip: -1, 256 | yflip: -1, 257 | brightness: -1, 258 | contrast: -1, 259 | dither565: -1, 260 | greyscale: -1, 261 | invert: -1, 262 | normalize: -1, 263 | posterize: -1, 264 | sepia: -1, 265 | fade: -1, 266 | blur: -1, 267 | gaussian: -1, 268 | pixelate: -1, 269 | circle: -1, 270 | quality: -1, 271 | }; 272 | -------------------------------------------------------------------------------- /server/src/controllers/image.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { v2 as cloudinary } from "cloudinary"; 3 | import { handleManipulateImage } from "../lib/image"; 4 | import { UserType } from "../schema/user"; 5 | import { 6 | addImageToDb, 7 | deleteImageFromDb, 8 | getAnyImageKeyById, 9 | getImagePrivacyById, 10 | getImagesFromDb, 11 | getImageKeyById, 12 | getNumOfImages, 13 | updateImagePrivacyToDb, 14 | } from "../db/user"; 15 | import { checkToken } from "../lib/user"; 16 | import { 17 | CLOUDINARY_API_KEY, 18 | CLOUDINARY_API_SECRET, 19 | CLOUDINARY_CLOUD_NAME, 20 | } from "../lib/constants"; 21 | import { sortQueryParamns } from "../utils/image"; 22 | import fs from "fs"; 23 | import path from "path"; 24 | import { CloudinaryImage } from "../constants/Image/cloudinary"; 25 | 26 | cloudinary.config({ 27 | cloud_name: CLOUDINARY_CLOUD_NAME, 28 | api_key: CLOUDINARY_API_KEY, 29 | api_secret: CLOUDINARY_API_SECRET, 30 | }); 31 | 32 | export const getImages = async (request: Request, response: Response) => { 33 | try { 34 | const user = response.locals.user as UserType; 35 | const images = await getImagesFromDb(user.id, true); 36 | const userId = user.id; 37 | const newImages = images.map((image) => { 38 | return { 39 | id: image.id, 40 | isPublic: image.isPublic, 41 | isOwner: image.createdById === userId, 42 | }; 43 | }); 44 | response.status(200).json(newImages); 45 | } catch (error) { 46 | response.status(500).json({ message: (error as Error).message }); 47 | } 48 | }; 49 | 50 | export const addImage = async (request: Request, response: Response) => { 51 | const imageDestination = request.file?.destination; 52 | const imageBuffer = fs.readFileSync( 53 | `${imageDestination}/${request.file?.filename}` 54 | ); 55 | if (!imageBuffer) { 56 | console.log("no file uploaded"); 57 | 58 | return response.status(400).json({ message: "No file uploaded" }); 59 | } 60 | const user = response.locals.user as UserType; 61 | try { 62 | if (!request.file) { 63 | return response.status(400).json({ message: "No file uploaded" }); 64 | } 65 | 66 | const numOfImages = await getNumOfImages(user.id); 67 | if (numOfImages >= 5) { 68 | return response.status(400).json({ 69 | message: 70 | "You have reached the maximum number of images, either delete an image or make a new account", 71 | }); 72 | } 73 | 74 | const key = `original/${`${user.id}-${new Date().getTime()}`}`; 75 | const imageData = await addImageToDb(key, user.id); 76 | const cloudinaryImage = new CloudinaryImage(key); 77 | await cloudinaryImage.uploadImage({ 78 | dbId: imageData.id, 79 | image: imageBuffer, 80 | }); 81 | fs.rmSync(`${imageDestination}/${request.file?.filename}`); 82 | response.status(200).json({ message: "Upload successful", ...imageData }); 83 | } catch (error) { 84 | console.log("error", error); 85 | response.status(500).json({ message: (error as Error).message }); 86 | } 87 | }; 88 | 89 | export const getImageById = async (request: Request, response: Response) => { 90 | try { 91 | const { public_id } = request.params; 92 | const searchParams = request.query; 93 | const selectedImg = await getAnyImageKeyById(public_id); 94 | const selectedimageKey = selectedImg.imageKey; 95 | 96 | const isPublic = selectedImg.isPublic; 97 | if (!selectedimageKey) { 98 | return response.status(404).json({ message: "Image not found" }); 99 | } 100 | if (!isPublic) { 101 | const userId = await checkToken(request, response); 102 | if (!userId || userId !== selectedImg.createdById) { 103 | return response.status(401).json({ message: "Unauthorized Access" }); 104 | } 105 | selectedImg.isOwner = selectedImg.id === userId; 106 | } 107 | try { 108 | if (Object.keys(searchParams).length > 0) { 109 | try { 110 | const { code } = sortQueryParamns(searchParams); 111 | const key = `filtered/${public_id}-${code}.png`; 112 | const image = new CloudinaryImage(key); 113 | const imageResponse = await image.downloadImage(); 114 | if (imageResponse) { 115 | response.writeHead(200, { 116 | "Content-Type": "image/png", 117 | "Content-Length": imageResponse.length, 118 | }); 119 | response.end(imageResponse); 120 | return; 121 | } 122 | } catch (error) {} 123 | } 124 | 125 | const image = new CloudinaryImage(selectedimageKey); 126 | const imageData = await image.downloadImage(); 127 | if (!imageData) { 128 | console.log("Image not found in Cloudinary"); 129 | 130 | return response.status(500).json({ message: "Failed to fetch image" }); 131 | } 132 | 133 | if (Object.keys(searchParams).length == 0) { 134 | response.writeHead(200, { 135 | "Content-Type": "image/png", 136 | "Content-Length": imageData.length, 137 | }); 138 | response.end(imageData); 139 | return; 140 | } 141 | 142 | const manipulatedImage = await handleManipulateImage( 143 | imageData, 144 | searchParams, 145 | selectedImg.id 146 | ); 147 | 148 | if (!manipulatedImage) { 149 | return response 150 | .status(500) 151 | .json({ message: "Failed to manipulate image" }); 152 | } 153 | const buffer = await manipulatedImage.getBufferAsync( 154 | manipulatedImage.getMIME() 155 | ); 156 | response.writeHead(200, { 157 | "Content-Type": manipulatedImage.getMIME(), 158 | "Content-Length": buffer.length, 159 | }); 160 | response.end(buffer); 161 | } catch (error) { 162 | response.status(500).json({ message: "Failed to fetch image" }); 163 | } 164 | } catch (error) { 165 | console.log("error", error); 166 | 167 | response.status(500).json({ error }); 168 | } 169 | }; 170 | 171 | export const getImageDetails = async (request: Request, response: Response) => { 172 | const { public_id } = request.params; 173 | const userId = await checkToken(request, response); 174 | if (!userId) { 175 | return response.status(401).json({ message: "Unauthorized Access" }); 176 | } 177 | const image = await getAnyImageKeyById(public_id); 178 | image.isOwner = image.createdById === userId; 179 | if (!image) { 180 | return response.status(404).json({ message: "Image not found" }); 181 | } 182 | if (image.createdById !== userId) { 183 | return response.status(401).json({ message: "Unauthorized Access" }); 184 | } 185 | response.status(200).json({ 186 | id: image.id, 187 | isPublic: image.isPublic, 188 | isOwner: image.isOwner, 189 | }); 190 | }; 191 | 192 | export const handleDeleteImage = async ( 193 | request: Request, 194 | response: Response 195 | ) => { 196 | const user = response.locals.user as UserType; 197 | const { public_id } = request.params; 198 | try { 199 | const imageKeyFromDb = await getImageKeyById(public_id, user.id); 200 | if (!imageKeyFromDb) { 201 | return response.status(404).json({ message: "Image not found" }); 202 | } 203 | const image = new CloudinaryImage(imageKeyFromDb); 204 | const deleteImage = await image.deleteImage(); 205 | if (deleteImage.result !== "ok") { 206 | return response.status(500).json({ message: "Failed to delete image" }); 207 | } 208 | const deleteImageId = await deleteImageFromDb(public_id, user.id); 209 | response 210 | .status(200) 211 | .json({ message: "Deletion successful", id: deleteImageId }); 212 | } catch (error) { 213 | response.status(500).json({ message: (error as Error).message }); 214 | } 215 | }; 216 | 217 | export const handleGetImagePrivacyStatus = async ( 218 | request: Request, 219 | response: Response 220 | ) => { 221 | const user = response.locals.user as UserType; 222 | const { public_id } = request.params; 223 | try { 224 | const isPublic = await getImagePrivacyById(public_id, user.id); 225 | if (isPublic != true && isPublic != false) { 226 | return response.status(404).json({ message: "Image not found" }); 227 | } 228 | response.status(200).json({ isPublic }); 229 | } catch (error) { 230 | response.status(500).json({ message: (error as Error).message }); 231 | } 232 | }; 233 | 234 | export const handleChangeImagePrivacy = async ( 235 | request: Request, 236 | response: Response 237 | ) => { 238 | const user = response.locals.user as UserType; 239 | const { public_id } = request.params; 240 | const { isPublic } = request.body; 241 | if (isPublic == undefined) { 242 | return response 243 | .status(400) 244 | .json({ message: "Please provide isPublic in the request body" }); 245 | } 246 | try { 247 | const imageKeyFromDb = await getImageKeyById(public_id, user.id); 248 | if (!imageKeyFromDb) { 249 | return response.status(404).json({ message: "Image not found" }); 250 | } 251 | const updatedImage = await updateImagePrivacyToDb( 252 | public_id, 253 | isPublic, 254 | user.id 255 | ); 256 | if (!updatedImage) { 257 | return response.status(500).json({ message: "Failed to update privacy" }); 258 | } 259 | 260 | response 261 | .status(200) 262 | .json({ message: "Privacy updated successfully", success: true }); 263 | } catch (error) { 264 | response.status(500).json({ message: (error as Error).message }); 265 | } 266 | }; 267 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 63 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 77 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 79 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 81 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 82 | 83 | /* Type Checking */ 84 | "strict": true, /* Enable all strict type-checking options. */ 85 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 86 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 87 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 90 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 91 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 92 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 93 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 94 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 95 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 96 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 97 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 98 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 99 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 100 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 101 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 102 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 103 | 104 | /* Completeness */ 105 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 106 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /client/src/assets/AuthGraphic.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthGraphic() { 2 | return ( 3 | 9 | 13 | 17 | 18 | 22 | 26 | 30 | 34 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 57 | 61 | 62 | 63 | 67 | 71 | 75 | 76 | 77 | 81 | 85 | 89 | 90 | 94 | 98 | 99 | 104 | 108 | 109 | 110 | 111 | 116 | 120 | 124 | 128 | 132 | 136 | 140 | 144 | 148 | 149 | 153 | 154 | 159 | 163 | 164 | 168 | 169 | 170 | 174 | 178 | 179 | 180 | 181 | 185 | 189 | 193 | 197 | 201 | 205 | 209 | 210 | 211 | ); 212 | } 213 | --------------------------------------------------------------------------------