├── .env.example ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── drizzle.config.ts ├── drizzle ├── 0000_burly_colonel_america.sql ├── 0001_little_mercury.sql ├── 0002_smiling_sphinx.sql ├── 0003_high_quasar.sql ├── 0004_clammy_prowler.sql ├── 0005_puzzling_vampiro.sql ├── 0006_stale_purple_man.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ ├── 0006_snapshot.json │ └── _journal.json ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── global.d.ts ├── hooks.server.ts ├── lib │ ├── components │ │ ├── buttons │ │ │ └── google-login-button.svelte │ │ ├── cards │ │ │ ├── image-picker-card.svelte │ │ │ └── result-card.svelte │ │ ├── compare-image.svelte │ │ ├── file-dropzone.svelte │ │ ├── footer.svelte │ │ ├── header.svelte │ │ ├── heading.svelte │ │ ├── privacy-policy.svx │ │ └── sidebar.svelte │ ├── constants │ │ └── price.ts │ ├── downloads.ts │ ├── icons │ │ ├── bg-replace-icon.svelte │ │ ├── biji-icon.svelte │ │ ├── bill-icon.svelte │ │ ├── buy-icon.svelte │ │ ├── circle-icon.svelte │ │ ├── close-icon.svelte │ │ ├── download-icon.svelte │ │ ├── download-multiple-icon.svelte │ │ ├── facebook-icon.svelte │ │ ├── file-upload-icon.svelte │ │ ├── github-icon.svelte │ │ ├── google-icon.svelte │ │ ├── hamburger-icon.svelte │ │ ├── home-icon.svelte │ │ ├── instagram-icon.svelte │ │ ├── invoice-icon.svelte │ │ ├── linkedin-icon.svelte │ │ ├── loading-icon.svelte │ │ ├── logout-icon.svelte │ │ ├── money-icon.svelte │ │ ├── reset-icon.svelte │ │ ├── trash-icon.svelte │ │ ├── twitter-icon.svelte │ │ ├── view-icon.svelte │ │ └── youtube-icon.svelte │ ├── image.ts │ ├── rate-limiter.ts │ ├── remove-bg.ts │ ├── schema │ │ ├── image-schema.ts │ │ └── payment-schema.ts │ ├── server │ │ ├── auth.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── oauth.ts │ │ ├── payment.ts │ │ └── user.ts │ ├── stores │ │ └── credit.svelte.ts │ ├── types │ │ └── tripay.ts │ └── utils.ts └── routes │ ├── (app) │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── app │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── invoices │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── pricing │ │ └── +page.svelte │ ├── privacy-policy │ │ └── +page.svelte │ └── topup │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── (landing-page) │ ├── +layout.svelte │ ├── +page.server.ts │ └── +page.svelte │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── api │ ├── payment │ │ └── callback │ │ │ └── +server.ts │ └── remove-biji │ │ └── +server.ts │ └── auth │ ├── login │ └── google │ │ ├── +server.ts │ │ └── callback │ │ └── +server.ts │ └── logout │ └── +server.ts ├── static ├── after.webp ├── before.webp └── favicon.ico ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Replace with your DB credentials! 2 | DATABASE_URL="postgres://user:password@host:port/db-name" 3 | 4 | PUBLIC_PLAUSIBLE_DATA_DOMAIN=your-domain.com 5 | 6 | BASE_URL=http://your-domain.com 7 | ORIGIN=https://your-domain.com 8 | BODY_SIZE_LIMIT=500M 9 | 10 | GOOGLE_CLIENT_ID= 11 | GOOGLE_CLIENT_SECRET= 12 | 13 | TRIPAY_API_KEY= 14 | TRIPAY_PRIVATE_KEY= 15 | TRIPAY_MERCHANT_CODE= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | @jsr:registry=https://npm.jsr.io 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); 3 | 4 | export default defineConfig({ 5 | schema: './src/lib/server/db/schema.ts', 6 | 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL 9 | }, 10 | 11 | verbose: true, 12 | strict: true, 13 | dialect: 'postgresql', 14 | casing: 'snake_case' 15 | }); 16 | -------------------------------------------------------------------------------- /drizzle/0000_burly_colonel_america.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "sessions" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "userId" integer NOT NULL, 4 | "expiresAt" timestamp with time zone NOT NULL 5 | ); 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "users" ( 8 | "id" serial PRIMARY KEY NOT NULL, 9 | "googleId" varchar NOT NULL, 10 | "email" varchar NOT NULL, 11 | "name" varchar, 12 | "picture" text, 13 | CONSTRAINT "users_googleId_unique" UNIQUE("googleId"), 14 | CONSTRAINT "users_email_unique" UNIQUE("email") 15 | ); 16 | --> statement-breakpoint 17 | DO $$ BEGIN 18 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; 19 | EXCEPTION 20 | WHEN duplicate_object THEN null; 21 | END $$; 22 | -------------------------------------------------------------------------------- /drizzle/0001_little_mercury.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "sessions" RENAME COLUMN "userId" TO "user_id";--> statement-breakpoint 2 | ALTER TABLE "sessions" RENAME COLUMN "expiresAt" TO "expires_at";--> statement-breakpoint 3 | ALTER TABLE "users" RENAME COLUMN "googleId" TO "google_id";--> statement-breakpoint 4 | ALTER TABLE "users" DROP CONSTRAINT "users_googleId_unique";--> statement-breakpoint 5 | ALTER TABLE "sessions" DROP CONSTRAINT "sessions_userId_users_id_fk"; 6 | --> statement-breakpoint 7 | DO $$ BEGIN 8 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; 9 | EXCEPTION 10 | WHEN duplicate_object THEN null; 11 | END $$; 12 | --> statement-breakpoint 13 | ALTER TABLE "users" ADD CONSTRAINT "users_googleId_unique" UNIQUE("google_id"); -------------------------------------------------------------------------------- /drizzle/0002_smiling_sphinx.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "credits" ( 2 | "id" integer PRIMARY KEY NOT NULL, 3 | "amount" integer DEFAULT 5 NOT NULL 4 | ); 5 | --> statement-breakpoint 6 | DO $$ BEGIN 7 | ALTER TABLE "credits" ADD CONSTRAINT "credits_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; 8 | EXCEPTION 9 | WHEN duplicate_object THEN null; 10 | END $$; 11 | -------------------------------------------------------------------------------- /drizzle/0003_high_quasar.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "invoices" ( 2 | "id" varchar PRIMARY KEY NOT NULL, 3 | "user_id" integer NOT NULL, 4 | "status" varchar DEFAULT 'UNPAID' NOT NULL, 5 | "expired_time" timestamp with time zone NOT NULL, 6 | "package" integer NOT NULL, 7 | "amount" integer NOT NULL 8 | ); 9 | --> statement-breakpoint 10 | DO $$ BEGIN 11 | ALTER TABLE "invoices" ADD CONSTRAINT "invoices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; 12 | EXCEPTION 13 | WHEN duplicate_object THEN null; 14 | END $$; 15 | -------------------------------------------------------------------------------- /drizzle/0004_clammy_prowler.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invoices" ADD COLUMN "paid_at" timestamp with time zone; -------------------------------------------------------------------------------- /drizzle/0005_puzzling_vampiro.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invoices" ADD COLUMN "created_at" timestamp with time zone DEFAULT now(); -------------------------------------------------------------------------------- /drizzle/0006_stale_purple_man.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invoices" ADD COLUMN "checkout_url" text; -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5cf321e1-5954-4adc-b17b-f442807367ad", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.sessions": { 8 | "name": "sessions", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "userId": { 18 | "name": "userId", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "expiresAt": { 24 | "name": "expiresAt", 25 | "type": "timestamp with time zone", 26 | "primaryKey": false, 27 | "notNull": true 28 | } 29 | }, 30 | "indexes": {}, 31 | "foreignKeys": { 32 | "sessions_userId_users_id_fk": { 33 | "name": "sessions_userId_users_id_fk", 34 | "tableFrom": "sessions", 35 | "tableTo": "users", 36 | "columnsFrom": ["userId"], 37 | "columnsTo": ["id"], 38 | "onDelete": "no action", 39 | "onUpdate": "no action" 40 | } 41 | }, 42 | "compositePrimaryKeys": {}, 43 | "uniqueConstraints": {}, 44 | "policies": {}, 45 | "checkConstraints": {}, 46 | "isRLSEnabled": false 47 | }, 48 | "public.users": { 49 | "name": "users", 50 | "schema": "", 51 | "columns": { 52 | "id": { 53 | "name": "id", 54 | "type": "serial", 55 | "primaryKey": true, 56 | "notNull": true 57 | }, 58 | "googleId": { 59 | "name": "googleId", 60 | "type": "varchar", 61 | "primaryKey": false, 62 | "notNull": true 63 | }, 64 | "email": { 65 | "name": "email", 66 | "type": "varchar", 67 | "primaryKey": false, 68 | "notNull": true 69 | }, 70 | "name": { 71 | "name": "name", 72 | "type": "varchar", 73 | "primaryKey": false, 74 | "notNull": false 75 | }, 76 | "picture": { 77 | "name": "picture", 78 | "type": "text", 79 | "primaryKey": false, 80 | "notNull": false 81 | } 82 | }, 83 | "indexes": {}, 84 | "foreignKeys": {}, 85 | "compositePrimaryKeys": {}, 86 | "uniqueConstraints": { 87 | "users_googleId_unique": { 88 | "name": "users_googleId_unique", 89 | "nullsNotDistinct": false, 90 | "columns": ["googleId"] 91 | }, 92 | "users_email_unique": { 93 | "name": "users_email_unique", 94 | "nullsNotDistinct": false, 95 | "columns": ["email"] 96 | } 97 | }, 98 | "policies": {}, 99 | "checkConstraints": {}, 100 | "isRLSEnabled": false 101 | } 102 | }, 103 | "enums": {}, 104 | "schemas": {}, 105 | "sequences": {}, 106 | "roles": {}, 107 | "policies": {}, 108 | "views": {}, 109 | "_meta": { 110 | "columns": {}, 111 | "schemas": {}, 112 | "tables": {} 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5dc43137-fb62-40b9-a5b3-dccafcdbbc0a", 3 | "prevId": "5cf321e1-5954-4adc-b17b-f442807367ad", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.sessions": { 8 | "name": "sessions", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "user_id": { 18 | "name": "user_id", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "expires_at": { 24 | "name": "expires_at", 25 | "type": "timestamp with time zone", 26 | "primaryKey": false, 27 | "notNull": true 28 | } 29 | }, 30 | "indexes": {}, 31 | "foreignKeys": { 32 | "sessions_user_id_users_id_fk": { 33 | "name": "sessions_user_id_users_id_fk", 34 | "tableFrom": "sessions", 35 | "tableTo": "users", 36 | "columnsFrom": ["user_id"], 37 | "columnsTo": ["id"], 38 | "onDelete": "no action", 39 | "onUpdate": "no action" 40 | } 41 | }, 42 | "compositePrimaryKeys": {}, 43 | "uniqueConstraints": {}, 44 | "policies": {}, 45 | "checkConstraints": {}, 46 | "isRLSEnabled": false 47 | }, 48 | "public.users": { 49 | "name": "users", 50 | "schema": "", 51 | "columns": { 52 | "id": { 53 | "name": "id", 54 | "type": "serial", 55 | "primaryKey": true, 56 | "notNull": true 57 | }, 58 | "google_id": { 59 | "name": "google_id", 60 | "type": "varchar", 61 | "primaryKey": false, 62 | "notNull": true 63 | }, 64 | "email": { 65 | "name": "email", 66 | "type": "varchar", 67 | "primaryKey": false, 68 | "notNull": true 69 | }, 70 | "name": { 71 | "name": "name", 72 | "type": "varchar", 73 | "primaryKey": false, 74 | "notNull": false 75 | }, 76 | "picture": { 77 | "name": "picture", 78 | "type": "text", 79 | "primaryKey": false, 80 | "notNull": false 81 | } 82 | }, 83 | "indexes": {}, 84 | "foreignKeys": {}, 85 | "compositePrimaryKeys": {}, 86 | "uniqueConstraints": { 87 | "users_googleId_unique": { 88 | "name": "users_googleId_unique", 89 | "nullsNotDistinct": false, 90 | "columns": ["google_id"] 91 | }, 92 | "users_email_unique": { 93 | "name": "users_email_unique", 94 | "nullsNotDistinct": false, 95 | "columns": ["email"] 96 | } 97 | }, 98 | "policies": {}, 99 | "checkConstraints": {}, 100 | "isRLSEnabled": false 101 | } 102 | }, 103 | "enums": {}, 104 | "schemas": {}, 105 | "sequences": {}, 106 | "roles": {}, 107 | "policies": {}, 108 | "views": {}, 109 | "_meta": { 110 | "columns": {}, 111 | "schemas": {}, 112 | "tables": {} 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9c8c6f4d-92b3-4ddb-b34c-23d12cd9b21d", 3 | "prevId": "5dc43137-fb62-40b9-a5b3-dccafcdbbc0a", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.credits": { 8 | "name": "credits", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "amount": { 18 | "name": "amount", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "default": 5 23 | } 24 | }, 25 | "indexes": {}, 26 | "foreignKeys": { 27 | "credits_id_users_id_fk": { 28 | "name": "credits_id_users_id_fk", 29 | "tableFrom": "credits", 30 | "tableTo": "users", 31 | "columnsFrom": [ 32 | "id" 33 | ], 34 | "columnsTo": [ 35 | "id" 36 | ], 37 | "onDelete": "no action", 38 | "onUpdate": "no action" 39 | } 40 | }, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | }, 47 | "public.sessions": { 48 | "name": "sessions", 49 | "schema": "", 50 | "columns": { 51 | "id": { 52 | "name": "id", 53 | "type": "text", 54 | "primaryKey": true, 55 | "notNull": true 56 | }, 57 | "user_id": { 58 | "name": "user_id", 59 | "type": "integer", 60 | "primaryKey": false, 61 | "notNull": true 62 | }, 63 | "expires_at": { 64 | "name": "expires_at", 65 | "type": "timestamp with time zone", 66 | "primaryKey": false, 67 | "notNull": true 68 | } 69 | }, 70 | "indexes": {}, 71 | "foreignKeys": { 72 | "sessions_user_id_users_id_fk": { 73 | "name": "sessions_user_id_users_id_fk", 74 | "tableFrom": "sessions", 75 | "tableTo": "users", 76 | "columnsFrom": [ 77 | "user_id" 78 | ], 79 | "columnsTo": [ 80 | "id" 81 | ], 82 | "onDelete": "no action", 83 | "onUpdate": "no action" 84 | } 85 | }, 86 | "compositePrimaryKeys": {}, 87 | "uniqueConstraints": {}, 88 | "policies": {}, 89 | "checkConstraints": {}, 90 | "isRLSEnabled": false 91 | }, 92 | "public.users": { 93 | "name": "users", 94 | "schema": "", 95 | "columns": { 96 | "id": { 97 | "name": "id", 98 | "type": "serial", 99 | "primaryKey": true, 100 | "notNull": true 101 | }, 102 | "google_id": { 103 | "name": "google_id", 104 | "type": "varchar", 105 | "primaryKey": false, 106 | "notNull": true 107 | }, 108 | "email": { 109 | "name": "email", 110 | "type": "varchar", 111 | "primaryKey": false, 112 | "notNull": true 113 | }, 114 | "name": { 115 | "name": "name", 116 | "type": "varchar", 117 | "primaryKey": false, 118 | "notNull": false 119 | }, 120 | "picture": { 121 | "name": "picture", 122 | "type": "text", 123 | "primaryKey": false, 124 | "notNull": false 125 | } 126 | }, 127 | "indexes": {}, 128 | "foreignKeys": {}, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": { 131 | "users_googleId_unique": { 132 | "name": "users_googleId_unique", 133 | "nullsNotDistinct": false, 134 | "columns": [ 135 | "google_id" 136 | ] 137 | }, 138 | "users_email_unique": { 139 | "name": "users_email_unique", 140 | "nullsNotDistinct": false, 141 | "columns": [ 142 | "email" 143 | ] 144 | } 145 | }, 146 | "policies": {}, 147 | "checkConstraints": {}, 148 | "isRLSEnabled": false 149 | } 150 | }, 151 | "enums": {}, 152 | "schemas": {}, 153 | "sequences": {}, 154 | "roles": {}, 155 | "policies": {}, 156 | "views": {}, 157 | "_meta": { 158 | "columns": {}, 159 | "schemas": {}, 160 | "tables": {} 161 | } 162 | } -------------------------------------------------------------------------------- /drizzle/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2feeb464-36dc-468b-adfa-5cbb4a409899", 3 | "prevId": "9c8c6f4d-92b3-4ddb-b34c-23d12cd9b21d", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.credits": { 8 | "name": "credits", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "amount": { 18 | "name": "amount", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "default": 5 23 | } 24 | }, 25 | "indexes": {}, 26 | "foreignKeys": { 27 | "credits_id_users_id_fk": { 28 | "name": "credits_id_users_id_fk", 29 | "tableFrom": "credits", 30 | "tableTo": "users", 31 | "columnsFrom": [ 32 | "id" 33 | ], 34 | "columnsTo": [ 35 | "id" 36 | ], 37 | "onDelete": "no action", 38 | "onUpdate": "no action" 39 | } 40 | }, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | }, 47 | "public.invoices": { 48 | "name": "invoices", 49 | "schema": "", 50 | "columns": { 51 | "id": { 52 | "name": "id", 53 | "type": "varchar", 54 | "primaryKey": true, 55 | "notNull": true 56 | }, 57 | "user_id": { 58 | "name": "user_id", 59 | "type": "integer", 60 | "primaryKey": false, 61 | "notNull": true 62 | }, 63 | "status": { 64 | "name": "status", 65 | "type": "varchar", 66 | "primaryKey": false, 67 | "notNull": true, 68 | "default": "'UNPAID'" 69 | }, 70 | "expired_time": { 71 | "name": "expired_time", 72 | "type": "timestamp with time zone", 73 | "primaryKey": false, 74 | "notNull": true 75 | }, 76 | "package": { 77 | "name": "package", 78 | "type": "integer", 79 | "primaryKey": false, 80 | "notNull": true 81 | }, 82 | "amount": { 83 | "name": "amount", 84 | "type": "integer", 85 | "primaryKey": false, 86 | "notNull": true 87 | } 88 | }, 89 | "indexes": {}, 90 | "foreignKeys": { 91 | "invoices_user_id_users_id_fk": { 92 | "name": "invoices_user_id_users_id_fk", 93 | "tableFrom": "invoices", 94 | "tableTo": "users", 95 | "columnsFrom": [ 96 | "user_id" 97 | ], 98 | "columnsTo": [ 99 | "id" 100 | ], 101 | "onDelete": "no action", 102 | "onUpdate": "no action" 103 | } 104 | }, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": {}, 107 | "policies": {}, 108 | "checkConstraints": {}, 109 | "isRLSEnabled": false 110 | }, 111 | "public.sessions": { 112 | "name": "sessions", 113 | "schema": "", 114 | "columns": { 115 | "id": { 116 | "name": "id", 117 | "type": "text", 118 | "primaryKey": true, 119 | "notNull": true 120 | }, 121 | "user_id": { 122 | "name": "user_id", 123 | "type": "integer", 124 | "primaryKey": false, 125 | "notNull": true 126 | }, 127 | "expires_at": { 128 | "name": "expires_at", 129 | "type": "timestamp with time zone", 130 | "primaryKey": false, 131 | "notNull": true 132 | } 133 | }, 134 | "indexes": {}, 135 | "foreignKeys": { 136 | "sessions_user_id_users_id_fk": { 137 | "name": "sessions_user_id_users_id_fk", 138 | "tableFrom": "sessions", 139 | "tableTo": "users", 140 | "columnsFrom": [ 141 | "user_id" 142 | ], 143 | "columnsTo": [ 144 | "id" 145 | ], 146 | "onDelete": "no action", 147 | "onUpdate": "no action" 148 | } 149 | }, 150 | "compositePrimaryKeys": {}, 151 | "uniqueConstraints": {}, 152 | "policies": {}, 153 | "checkConstraints": {}, 154 | "isRLSEnabled": false 155 | }, 156 | "public.users": { 157 | "name": "users", 158 | "schema": "", 159 | "columns": { 160 | "id": { 161 | "name": "id", 162 | "type": "serial", 163 | "primaryKey": true, 164 | "notNull": true 165 | }, 166 | "google_id": { 167 | "name": "google_id", 168 | "type": "varchar", 169 | "primaryKey": false, 170 | "notNull": true 171 | }, 172 | "email": { 173 | "name": "email", 174 | "type": "varchar", 175 | "primaryKey": false, 176 | "notNull": true 177 | }, 178 | "name": { 179 | "name": "name", 180 | "type": "varchar", 181 | "primaryKey": false, 182 | "notNull": false 183 | }, 184 | "picture": { 185 | "name": "picture", 186 | "type": "text", 187 | "primaryKey": false, 188 | "notNull": false 189 | } 190 | }, 191 | "indexes": {}, 192 | "foreignKeys": {}, 193 | "compositePrimaryKeys": {}, 194 | "uniqueConstraints": { 195 | "users_googleId_unique": { 196 | "name": "users_googleId_unique", 197 | "nullsNotDistinct": false, 198 | "columns": [ 199 | "google_id" 200 | ] 201 | }, 202 | "users_email_unique": { 203 | "name": "users_email_unique", 204 | "nullsNotDistinct": false, 205 | "columns": [ 206 | "email" 207 | ] 208 | } 209 | }, 210 | "policies": {}, 211 | "checkConstraints": {}, 212 | "isRLSEnabled": false 213 | } 214 | }, 215 | "enums": {}, 216 | "schemas": {}, 217 | "sequences": {}, 218 | "roles": {}, 219 | "policies": {}, 220 | "views": {}, 221 | "_meta": { 222 | "columns": {}, 223 | "schemas": {}, 224 | "tables": {} 225 | } 226 | } -------------------------------------------------------------------------------- /drizzle/meta/0004_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "956c2f32-007a-43db-a459-323edb41f43f", 3 | "prevId": "2feeb464-36dc-468b-adfa-5cbb4a409899", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.credits": { 8 | "name": "credits", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "amount": { 18 | "name": "amount", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "default": 5 23 | } 24 | }, 25 | "indexes": {}, 26 | "foreignKeys": { 27 | "credits_id_users_id_fk": { 28 | "name": "credits_id_users_id_fk", 29 | "tableFrom": "credits", 30 | "tableTo": "users", 31 | "columnsFrom": [ 32 | "id" 33 | ], 34 | "columnsTo": [ 35 | "id" 36 | ], 37 | "onDelete": "no action", 38 | "onUpdate": "no action" 39 | } 40 | }, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | }, 47 | "public.invoices": { 48 | "name": "invoices", 49 | "schema": "", 50 | "columns": { 51 | "id": { 52 | "name": "id", 53 | "type": "varchar", 54 | "primaryKey": true, 55 | "notNull": true 56 | }, 57 | "user_id": { 58 | "name": "user_id", 59 | "type": "integer", 60 | "primaryKey": false, 61 | "notNull": true 62 | }, 63 | "status": { 64 | "name": "status", 65 | "type": "varchar", 66 | "primaryKey": false, 67 | "notNull": true, 68 | "default": "'UNPAID'" 69 | }, 70 | "expired_time": { 71 | "name": "expired_time", 72 | "type": "timestamp with time zone", 73 | "primaryKey": false, 74 | "notNull": true 75 | }, 76 | "paid_at": { 77 | "name": "paid_at", 78 | "type": "timestamp with time zone", 79 | "primaryKey": false, 80 | "notNull": false 81 | }, 82 | "package": { 83 | "name": "package", 84 | "type": "integer", 85 | "primaryKey": false, 86 | "notNull": true 87 | }, 88 | "amount": { 89 | "name": "amount", 90 | "type": "integer", 91 | "primaryKey": false, 92 | "notNull": true 93 | } 94 | }, 95 | "indexes": {}, 96 | "foreignKeys": { 97 | "invoices_user_id_users_id_fk": { 98 | "name": "invoices_user_id_users_id_fk", 99 | "tableFrom": "invoices", 100 | "tableTo": "users", 101 | "columnsFrom": [ 102 | "user_id" 103 | ], 104 | "columnsTo": [ 105 | "id" 106 | ], 107 | "onDelete": "no action", 108 | "onUpdate": "no action" 109 | } 110 | }, 111 | "compositePrimaryKeys": {}, 112 | "uniqueConstraints": {}, 113 | "policies": {}, 114 | "checkConstraints": {}, 115 | "isRLSEnabled": false 116 | }, 117 | "public.sessions": { 118 | "name": "sessions", 119 | "schema": "", 120 | "columns": { 121 | "id": { 122 | "name": "id", 123 | "type": "text", 124 | "primaryKey": true, 125 | "notNull": true 126 | }, 127 | "user_id": { 128 | "name": "user_id", 129 | "type": "integer", 130 | "primaryKey": false, 131 | "notNull": true 132 | }, 133 | "expires_at": { 134 | "name": "expires_at", 135 | "type": "timestamp with time zone", 136 | "primaryKey": false, 137 | "notNull": true 138 | } 139 | }, 140 | "indexes": {}, 141 | "foreignKeys": { 142 | "sessions_user_id_users_id_fk": { 143 | "name": "sessions_user_id_users_id_fk", 144 | "tableFrom": "sessions", 145 | "tableTo": "users", 146 | "columnsFrom": [ 147 | "user_id" 148 | ], 149 | "columnsTo": [ 150 | "id" 151 | ], 152 | "onDelete": "no action", 153 | "onUpdate": "no action" 154 | } 155 | }, 156 | "compositePrimaryKeys": {}, 157 | "uniqueConstraints": {}, 158 | "policies": {}, 159 | "checkConstraints": {}, 160 | "isRLSEnabled": false 161 | }, 162 | "public.users": { 163 | "name": "users", 164 | "schema": "", 165 | "columns": { 166 | "id": { 167 | "name": "id", 168 | "type": "serial", 169 | "primaryKey": true, 170 | "notNull": true 171 | }, 172 | "google_id": { 173 | "name": "google_id", 174 | "type": "varchar", 175 | "primaryKey": false, 176 | "notNull": true 177 | }, 178 | "email": { 179 | "name": "email", 180 | "type": "varchar", 181 | "primaryKey": false, 182 | "notNull": true 183 | }, 184 | "name": { 185 | "name": "name", 186 | "type": "varchar", 187 | "primaryKey": false, 188 | "notNull": false 189 | }, 190 | "picture": { 191 | "name": "picture", 192 | "type": "text", 193 | "primaryKey": false, 194 | "notNull": false 195 | } 196 | }, 197 | "indexes": {}, 198 | "foreignKeys": {}, 199 | "compositePrimaryKeys": {}, 200 | "uniqueConstraints": { 201 | "users_googleId_unique": { 202 | "name": "users_googleId_unique", 203 | "nullsNotDistinct": false, 204 | "columns": [ 205 | "google_id" 206 | ] 207 | }, 208 | "users_email_unique": { 209 | "name": "users_email_unique", 210 | "nullsNotDistinct": false, 211 | "columns": [ 212 | "email" 213 | ] 214 | } 215 | }, 216 | "policies": {}, 217 | "checkConstraints": {}, 218 | "isRLSEnabled": false 219 | } 220 | }, 221 | "enums": {}, 222 | "schemas": {}, 223 | "sequences": {}, 224 | "roles": {}, 225 | "policies": {}, 226 | "views": {}, 227 | "_meta": { 228 | "columns": {}, 229 | "schemas": {}, 230 | "tables": {} 231 | } 232 | } -------------------------------------------------------------------------------- /drizzle/meta/0005_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "01f7a8c0-a363-4c0d-8a0b-a2121790eb3e", 3 | "prevId": "956c2f32-007a-43db-a459-323edb41f43f", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.credits": { 8 | "name": "credits", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "amount": { 18 | "name": "amount", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "default": 5 23 | } 24 | }, 25 | "indexes": {}, 26 | "foreignKeys": { 27 | "credits_id_users_id_fk": { 28 | "name": "credits_id_users_id_fk", 29 | "tableFrom": "credits", 30 | "tableTo": "users", 31 | "columnsFrom": [ 32 | "id" 33 | ], 34 | "columnsTo": [ 35 | "id" 36 | ], 37 | "onDelete": "no action", 38 | "onUpdate": "no action" 39 | } 40 | }, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | }, 47 | "public.invoices": { 48 | "name": "invoices", 49 | "schema": "", 50 | "columns": { 51 | "id": { 52 | "name": "id", 53 | "type": "varchar", 54 | "primaryKey": true, 55 | "notNull": true 56 | }, 57 | "user_id": { 58 | "name": "user_id", 59 | "type": "integer", 60 | "primaryKey": false, 61 | "notNull": true 62 | }, 63 | "status": { 64 | "name": "status", 65 | "type": "varchar", 66 | "primaryKey": false, 67 | "notNull": true, 68 | "default": "'UNPAID'" 69 | }, 70 | "expired_time": { 71 | "name": "expired_time", 72 | "type": "timestamp with time zone", 73 | "primaryKey": false, 74 | "notNull": true 75 | }, 76 | "paid_at": { 77 | "name": "paid_at", 78 | "type": "timestamp with time zone", 79 | "primaryKey": false, 80 | "notNull": false 81 | }, 82 | "package": { 83 | "name": "package", 84 | "type": "integer", 85 | "primaryKey": false, 86 | "notNull": true 87 | }, 88 | "amount": { 89 | "name": "amount", 90 | "type": "integer", 91 | "primaryKey": false, 92 | "notNull": true 93 | }, 94 | "created_at": { 95 | "name": "created_at", 96 | "type": "timestamp with time zone", 97 | "primaryKey": false, 98 | "notNull": false, 99 | "default": "now()" 100 | } 101 | }, 102 | "indexes": {}, 103 | "foreignKeys": { 104 | "invoices_user_id_users_id_fk": { 105 | "name": "invoices_user_id_users_id_fk", 106 | "tableFrom": "invoices", 107 | "tableTo": "users", 108 | "columnsFrom": [ 109 | "user_id" 110 | ], 111 | "columnsTo": [ 112 | "id" 113 | ], 114 | "onDelete": "no action", 115 | "onUpdate": "no action" 116 | } 117 | }, 118 | "compositePrimaryKeys": {}, 119 | "uniqueConstraints": {}, 120 | "policies": {}, 121 | "checkConstraints": {}, 122 | "isRLSEnabled": false 123 | }, 124 | "public.sessions": { 125 | "name": "sessions", 126 | "schema": "", 127 | "columns": { 128 | "id": { 129 | "name": "id", 130 | "type": "text", 131 | "primaryKey": true, 132 | "notNull": true 133 | }, 134 | "user_id": { 135 | "name": "user_id", 136 | "type": "integer", 137 | "primaryKey": false, 138 | "notNull": true 139 | }, 140 | "expires_at": { 141 | "name": "expires_at", 142 | "type": "timestamp with time zone", 143 | "primaryKey": false, 144 | "notNull": true 145 | } 146 | }, 147 | "indexes": {}, 148 | "foreignKeys": { 149 | "sessions_user_id_users_id_fk": { 150 | "name": "sessions_user_id_users_id_fk", 151 | "tableFrom": "sessions", 152 | "tableTo": "users", 153 | "columnsFrom": [ 154 | "user_id" 155 | ], 156 | "columnsTo": [ 157 | "id" 158 | ], 159 | "onDelete": "no action", 160 | "onUpdate": "no action" 161 | } 162 | }, 163 | "compositePrimaryKeys": {}, 164 | "uniqueConstraints": {}, 165 | "policies": {}, 166 | "checkConstraints": {}, 167 | "isRLSEnabled": false 168 | }, 169 | "public.users": { 170 | "name": "users", 171 | "schema": "", 172 | "columns": { 173 | "id": { 174 | "name": "id", 175 | "type": "serial", 176 | "primaryKey": true, 177 | "notNull": true 178 | }, 179 | "google_id": { 180 | "name": "google_id", 181 | "type": "varchar", 182 | "primaryKey": false, 183 | "notNull": true 184 | }, 185 | "email": { 186 | "name": "email", 187 | "type": "varchar", 188 | "primaryKey": false, 189 | "notNull": true 190 | }, 191 | "name": { 192 | "name": "name", 193 | "type": "varchar", 194 | "primaryKey": false, 195 | "notNull": false 196 | }, 197 | "picture": { 198 | "name": "picture", 199 | "type": "text", 200 | "primaryKey": false, 201 | "notNull": false 202 | } 203 | }, 204 | "indexes": {}, 205 | "foreignKeys": {}, 206 | "compositePrimaryKeys": {}, 207 | "uniqueConstraints": { 208 | "users_googleId_unique": { 209 | "name": "users_googleId_unique", 210 | "nullsNotDistinct": false, 211 | "columns": [ 212 | "google_id" 213 | ] 214 | }, 215 | "users_email_unique": { 216 | "name": "users_email_unique", 217 | "nullsNotDistinct": false, 218 | "columns": [ 219 | "email" 220 | ] 221 | } 222 | }, 223 | "policies": {}, 224 | "checkConstraints": {}, 225 | "isRLSEnabled": false 226 | } 227 | }, 228 | "enums": {}, 229 | "schemas": {}, 230 | "sequences": {}, 231 | "roles": {}, 232 | "policies": {}, 233 | "views": {}, 234 | "_meta": { 235 | "columns": {}, 236 | "schemas": {}, 237 | "tables": {} 238 | } 239 | } -------------------------------------------------------------------------------- /drizzle/meta/0006_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "66a52ed4-3e8e-4af6-b111-6504ed56f93d", 3 | "prevId": "01f7a8c0-a363-4c0d-8a0b-a2121790eb3e", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.credits": { 8 | "name": "credits", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "amount": { 18 | "name": "amount", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "default": 5 23 | } 24 | }, 25 | "indexes": {}, 26 | "foreignKeys": { 27 | "credits_id_users_id_fk": { 28 | "name": "credits_id_users_id_fk", 29 | "tableFrom": "credits", 30 | "tableTo": "users", 31 | "columnsFrom": [ 32 | "id" 33 | ], 34 | "columnsTo": [ 35 | "id" 36 | ], 37 | "onDelete": "no action", 38 | "onUpdate": "no action" 39 | } 40 | }, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | }, 47 | "public.invoices": { 48 | "name": "invoices", 49 | "schema": "", 50 | "columns": { 51 | "id": { 52 | "name": "id", 53 | "type": "varchar", 54 | "primaryKey": true, 55 | "notNull": true 56 | }, 57 | "user_id": { 58 | "name": "user_id", 59 | "type": "integer", 60 | "primaryKey": false, 61 | "notNull": true 62 | }, 63 | "status": { 64 | "name": "status", 65 | "type": "varchar", 66 | "primaryKey": false, 67 | "notNull": true, 68 | "default": "'UNPAID'" 69 | }, 70 | "expired_time": { 71 | "name": "expired_time", 72 | "type": "timestamp with time zone", 73 | "primaryKey": false, 74 | "notNull": true 75 | }, 76 | "paid_at": { 77 | "name": "paid_at", 78 | "type": "timestamp with time zone", 79 | "primaryKey": false, 80 | "notNull": false 81 | }, 82 | "package": { 83 | "name": "package", 84 | "type": "integer", 85 | "primaryKey": false, 86 | "notNull": true 87 | }, 88 | "amount": { 89 | "name": "amount", 90 | "type": "integer", 91 | "primaryKey": false, 92 | "notNull": true 93 | }, 94 | "checkout_url": { 95 | "name": "checkout_url", 96 | "type": "text", 97 | "primaryKey": false, 98 | "notNull": false 99 | }, 100 | "created_at": { 101 | "name": "created_at", 102 | "type": "timestamp with time zone", 103 | "primaryKey": false, 104 | "notNull": false, 105 | "default": "now()" 106 | } 107 | }, 108 | "indexes": {}, 109 | "foreignKeys": { 110 | "invoices_user_id_users_id_fk": { 111 | "name": "invoices_user_id_users_id_fk", 112 | "tableFrom": "invoices", 113 | "tableTo": "users", 114 | "columnsFrom": [ 115 | "user_id" 116 | ], 117 | "columnsTo": [ 118 | "id" 119 | ], 120 | "onDelete": "no action", 121 | "onUpdate": "no action" 122 | } 123 | }, 124 | "compositePrimaryKeys": {}, 125 | "uniqueConstraints": {}, 126 | "policies": {}, 127 | "checkConstraints": {}, 128 | "isRLSEnabled": false 129 | }, 130 | "public.sessions": { 131 | "name": "sessions", 132 | "schema": "", 133 | "columns": { 134 | "id": { 135 | "name": "id", 136 | "type": "text", 137 | "primaryKey": true, 138 | "notNull": true 139 | }, 140 | "user_id": { 141 | "name": "user_id", 142 | "type": "integer", 143 | "primaryKey": false, 144 | "notNull": true 145 | }, 146 | "expires_at": { 147 | "name": "expires_at", 148 | "type": "timestamp with time zone", 149 | "primaryKey": false, 150 | "notNull": true 151 | } 152 | }, 153 | "indexes": {}, 154 | "foreignKeys": { 155 | "sessions_user_id_users_id_fk": { 156 | "name": "sessions_user_id_users_id_fk", 157 | "tableFrom": "sessions", 158 | "tableTo": "users", 159 | "columnsFrom": [ 160 | "user_id" 161 | ], 162 | "columnsTo": [ 163 | "id" 164 | ], 165 | "onDelete": "no action", 166 | "onUpdate": "no action" 167 | } 168 | }, 169 | "compositePrimaryKeys": {}, 170 | "uniqueConstraints": {}, 171 | "policies": {}, 172 | "checkConstraints": {}, 173 | "isRLSEnabled": false 174 | }, 175 | "public.users": { 176 | "name": "users", 177 | "schema": "", 178 | "columns": { 179 | "id": { 180 | "name": "id", 181 | "type": "serial", 182 | "primaryKey": true, 183 | "notNull": true 184 | }, 185 | "google_id": { 186 | "name": "google_id", 187 | "type": "varchar", 188 | "primaryKey": false, 189 | "notNull": true 190 | }, 191 | "email": { 192 | "name": "email", 193 | "type": "varchar", 194 | "primaryKey": false, 195 | "notNull": true 196 | }, 197 | "name": { 198 | "name": "name", 199 | "type": "varchar", 200 | "primaryKey": false, 201 | "notNull": false 202 | }, 203 | "picture": { 204 | "name": "picture", 205 | "type": "text", 206 | "primaryKey": false, 207 | "notNull": false 208 | } 209 | }, 210 | "indexes": {}, 211 | "foreignKeys": {}, 212 | "compositePrimaryKeys": {}, 213 | "uniqueConstraints": { 214 | "users_googleId_unique": { 215 | "name": "users_googleId_unique", 216 | "nullsNotDistinct": false, 217 | "columns": [ 218 | "google_id" 219 | ] 220 | }, 221 | "users_email_unique": { 222 | "name": "users_email_unique", 223 | "nullsNotDistinct": false, 224 | "columns": [ 225 | "email" 226 | ] 227 | } 228 | }, 229 | "policies": {}, 230 | "checkConstraints": {}, 231 | "isRLSEnabled": false 232 | } 233 | }, 234 | "enums": {}, 235 | "schemas": {}, 236 | "sequences": {}, 237 | "roles": {}, 238 | "policies": {}, 239 | "views": {}, 240 | "_meta": { 241 | "columns": {}, 242 | "schemas": {}, 243 | "tables": {} 244 | } 245 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1731838338416, 9 | "tag": "0000_burly_colonel_america", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1731838510220, 16 | "tag": "0001_little_mercury", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1731923769234, 23 | "tag": "0002_smiling_sphinx", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "7", 29 | "when": 1731978302985, 30 | "tag": "0003_high_quasar", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "7", 36 | "when": 1731979053822, 37 | "tag": "0004_clammy_prowler", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "7", 43 | "when": 1731981368729, 44 | "tag": "0005_puzzling_vampiro", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "7", 50 | "when": 1731984952998, 51 | "tag": "0006_stale_purple_man", 52 | "breakpoints": true 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import ts from 'typescript-eslint'; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte', '**/*.svx'], 23 | 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remove-biji", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "start": "drizzle-kit migrate && node build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "format": "prettier --write .", 13 | "lint": "prettier --check . && eslint .", 14 | "db:push": "drizzle-kit push", 15 | "db:generate": "drizzle-kit generate", 16 | "db:migrate": "drizzle-kit migrate", 17 | "db:studio": "drizzle-kit studio" 18 | }, 19 | "devDependencies": { 20 | "@floating-ui/dom": "^1.6.12", 21 | "@node-rs/argon2": "^2.0.0", 22 | "@oslojs/crypto": "^1.0.1", 23 | "@oslojs/encoding": "^1.1.0", 24 | "@pilcrowjs/object-parser": "^0.0.4", 25 | "@skeletonlabs/tw-plugin": "^0.4.0", 26 | "@sveltejs/adapter-node": "^5.2.9", 27 | "@sveltejs/kit": "^2.8.3", 28 | "@sveltejs/vite-plugin-svelte": "^4.0.2", 29 | "@tailwindcss/typography": "^0.5.15", 30 | "@types/eslint": "^9.6.1", 31 | "@types/node": "^22.9.3", 32 | "arctic": "^2.3.0", 33 | "autoprefixer": "^10.4.20", 34 | "drizzle-kit": "^0.28.1", 35 | "eslint": "9.14.0", 36 | "eslint-config-prettier": "^9.1.0", 37 | "eslint-plugin-svelte": "^2.46.0", 38 | "globals": "^15.12.0", 39 | "mdsvex": "^0.11.2", 40 | "postgres": "^3.4.5", 41 | "prettier": "^3.3.3", 42 | "prettier-plugin-svelte": "^3.3.2", 43 | "prettier-plugin-tailwindcss": "^0.6.9", 44 | "svelte": "^5.2.7", 45 | "svelte-check": "^4.1.0", 46 | "tailwindcss": "^3.4.15", 47 | "typescript": "^5.7.2", 48 | "typescript-eslint": "^8.15.0", 49 | "vite": "^5.4.11" 50 | }, 51 | "dependencies": { 52 | "@fontsource/roboto": "^5.1.0", 53 | "@iconify/svelte": "^4.0.2", 54 | "@imgly/background-removal-node": "^1.4.5", 55 | "@tfkhdyt/with-catch": "npm:@jsr/tfkhdyt__with-catch@^0.3.0", 56 | "@skeletonlabs/skeleton": "^2.10.3", 57 | "clsx": "^2.1.1", 58 | "date-fns": "^4.1.0", 59 | "drizzle-orm": "^0.36.4", 60 | "jszip": "^3.10.1", 61 | "sharp": "^0.33.5", 62 | "svelte-french-toast": "^1.2.0", 63 | "svelte-seo": "^1.6.1", 64 | "sveltekit-rate-limiter": "^0.6.1", 65 | "sveltekit-superforms": "^2.20.1", 66 | "sveltekit-view-transition": "^0.5.3", 67 | "ts-pattern": "^5.5.0", 68 | "uuid": "^11.0.3", 69 | "zod": "^3.23.8" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | /* NOTE: set your target theme name (ex: skeleton, wintry, modern, etc) */ 6 | 7 | :root [data-theme='skeleton'] { 8 | --theme-font-family-base: 'Roboto', sans-serif; 9 | --theme-font-family-heading: 'Roboto', sans-serif; 10 | /* ... */ 11 | } 12 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | interface Locals { 6 | user: import('$lib/server/auth').SessionValidationResult['user']; 7 | session: import('$lib/server/auth').SessionValidationResult['session']; 8 | } 9 | 10 | namespace Superforms { 11 | type Message = { 12 | type: 'error' | 'success'; 13 | message: string; 14 | }; 15 | } 16 | } 17 | } 18 | 19 | export {}; 20 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svx' { 2 | import type { SvelteComponent } from 'svelte'; 3 | 4 | export default class Comp extends SvelteComponent {} 5 | 6 | export const metadata: Record; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import * as auth from '$lib/server/auth.js'; 3 | 4 | const handleAuth: Handle = async ({ event, resolve }) => { 5 | const sessionToken = event.cookies.get(auth.sessionCookieName); 6 | if (!sessionToken) { 7 | event.locals.user = null; 8 | event.locals.session = null; 9 | return resolve(event); 10 | } 11 | 12 | const { session, user } = await auth.validateSessionToken(sessionToken); 13 | if (session) { 14 | auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); 15 | } else { 16 | auth.deleteSessionTokenCookie(event); 17 | } 18 | 19 | event.locals.user = user; 20 | event.locals.session = session; 21 | 22 | return resolve(event); 23 | }; 24 | 25 | export const handle: Handle = handleAuth; 26 | -------------------------------------------------------------------------------- /src/lib/components/buttons/google-login-button.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | Login dengan Google 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/cards/image-picker-card.svelte: -------------------------------------------------------------------------------- 1 | 64 | 65 |
66 |
67 | 68 |
69 |
70 | {#if images && images.length > 0} 71 |
'grid-cols-1') 76 | .otherwise(() => 'grid-cols-4') 77 | )} 78 | > 79 | {#each images as image, index (image.name)} 80 |
85 |
86 | {image.name} 94 | {#if outputs.length === 0 && !isLoading} 95 |
102 | 108 | {/if} 109 |
110 |
111 | {/each} 112 |
113 | {:else} 114 |
115 | 116 |
117 | {/if} 118 |
119 | {#if isImageExist} 120 |
121 | 134 | {#if outputs.length === 0} 135 | 144 | {/if} 145 |
146 | {/if} 147 |
148 | -------------------------------------------------------------------------------- /src/lib/components/cards/result-card.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |
Hasil
33 |
34 | {#if outputs && outputs.length > 0} 35 |
'grid-cols-1') 41 | .otherwise(() => 'grid-cols-4') 42 | )} 43 | > 44 | 45 | {#each outputs as _, i (i)} 46 |
47 | output {i + 1} 55 |
58 | 63 | {#if outputs.length > 1} 64 | 69 | {/if} 70 |
71 |
72 | {/each} 73 |
74 | {:else if isLoading} 75 |
76 |
77 | 78 |

Sabar bang, lagi diproses

79 |
80 |
81 | {:else} 82 |
83 |

Hasil akan muncul di sini

84 |
85 | {/if} 86 |
87 | {#if outputs && outputs.length > 0} 88 |
89 | {#if outputs.length > 1} 90 | 98 | {:else if outputs.length === 1} 99 | 107 | {/if} 108 |
109 | {/if} 110 |
111 | -------------------------------------------------------------------------------- /src/lib/components/compare-image.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
26 | {imageLeftAlt} 27 | {imageRightAlt} 28 | 29 | 45 |
46 | 47 | 176 | -------------------------------------------------------------------------------- /src/lib/components/file-dropzone.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 23 | Pilih gambar atau drag and drop 24 | Hanya JPEG, PNG, dan WebP (Maksimal 5MB) 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/footer.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 | 68 | -------------------------------------------------------------------------------- /src/lib/components/header.svelte: -------------------------------------------------------------------------------- 1 | 60 | 61 |
62 |
63 | 64 |
65 | 66 | 67 |
68 | Remove Biji 69 |
70 |

Hilangkan bijimu dengan mudah

71 |
72 | 105 |
106 | {#if user} 107 |
108 | 109 | {#if creditStore.amount !== null} 110 | {creditStore.amount} 111 | {:else} 112 | 113 | {/if} 114 |
115 | {/if} 116 | 117 | 118 |
119 |
120 | 121 | 122 |
123 |
124 |

Hello, {user?.name}

125 | 126 | 127 | 128 | Logout 130 |
131 |
132 |
133 | 134 | 135 | -------------------------------------------------------------------------------- /src/lib/components/heading.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |

{title}

16 | {#if subtitle} 17 |

{subtitle}

18 | {/if} 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/privacy-policy.svx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy Policy 3 | description: Privacy Policy for Remove Biji 4 | --- 5 | 6 | # Privacy Policy for Remove Biji 7 | 8 | _Last Updated: November 21, 2024_ 9 | 10 | ## Introduction 11 | 12 | Welcome to Remove Biji ("we," "our," or "us"). We are committed to protecting your privacy and ensuring you have a positive experience when using our background removal service. This privacy policy explains how we collect, use, and safeguard your information when you use our web-based application. 13 | 14 | ## Information We Collect 15 | 16 | ### Account Information 17 | 18 | When you use Remove Biji, we collect the following information through Google Sign-In: 19 | 20 | - Google ID 21 | - Email address 22 | - Name 23 | - Profile picture 24 | 25 | ### Technical Information 26 | 27 | We automatically collect certain information about your device when you access our service, including: 28 | 29 | - Browser type and version 30 | - Operating system 31 | - IP address 32 | - Access timestamps 33 | - Browser settings 34 | 35 | ## How We Use Your Information 36 | 37 | We use your personal information for the following purposes: 38 | 39 | - To create and manage your user account 40 | - To provide our background removal service 41 | - To authenticate your identity 42 | - To improve and optimize our application 43 | - To protect against fraud and unauthorized access 44 | 45 | ## Image Processing and Storage 46 | 47 | We want to be transparent about how we handle your images: 48 | 49 | - We process your images in real-time for background removal 50 | - We DO NOT store or retain any uploaded images 51 | - Images are automatically deleted after processing 52 | - We DO NOT use your images for any other purposes 53 | 54 | ## Payment Processing 55 | 56 | For payment processing: 57 | 58 | - We use secure third-party payment gateway 59 | - We DO NOT collect or store any payment information 60 | - All payment transactions are encrypted and processed through our payment gateway partner 61 | - Please refer to the payment gateway's privacy policy for information about how they handle your payment data 62 | 63 | ## Data Sharing and Disclosure 64 | 65 | We do not sell, trade, or rent your personal information to third parties. We may share your information only in the following circumstances: 66 | 67 | - When required by law 68 | - To protect our rights and property 69 | - To prevent fraud or illegal activities 70 | - With your explicit consent 71 | 72 | ## Data Security 73 | 74 | We implement appropriate technical and organizational security measures to protect your personal information, including: 75 | 76 | - Secure server infrastructure 77 | - Access controls and authentication measures 78 | 79 | ## Your Rights 80 | 81 | You have the right to: 82 | 83 | - Access your personal information 84 | - Correct inaccurate or incomplete information 85 | - Request deletion of your account and associated data 86 | - Export your data 87 | - Withdraw consent at any time 88 | 89 | ## Children's Privacy 90 | 91 | Remove Biji is intended for general audiences and does not knowingly collect personal information from children under 13. If we learn that we have collected personal information from a child under 13, we will take steps to delete such information. 92 | 93 | ## Changes to This Policy 94 | 95 | We may update this privacy policy from time to time. We will notify you of any changes by: 96 | 97 | - Posting the new privacy policy on our website 98 | - Updating the "Last Updated" date at the top of this policy 99 | 100 | ## Contact Us 101 | 102 | If you have any questions about this privacy policy or our practices, please contact us at: 103 | 104 | - Email: tfkhdyt@proton.me 105 | - Facebook: https://www.facebook.com/tfkhdyt142 106 | 107 | ## Compliance 108 | 109 | This privacy policy complies with applicable data protection laws and regulations, including: 110 | 111 | - General Data Protection Regulation (GDPR) 112 | - California Consumer Privacy Act (CCPA) 113 | - Personal Information Protection and Electronic Documents Act (PIPEDA) 114 | -------------------------------------------------------------------------------- /src/lib/components/sidebar.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | {#if isOpen} 34 | 35 | 36 | 37 |
42 | {/if} 43 | 44 | 45 |
51 | 54 |
55 | {#if user} 56 |
57 |
Jumlah bijimu
58 |
59 | {#if creditStore.amount !== null} 60 | 61 | {creditStore.amount} 62 | {:else} 63 | 64 | {/if} 65 |
66 |
67 | 71 | Top Up 72 | 73 | 74 | 78 | Purchases 79 | 80 | 81 | {/if} 82 | 86 | Pricing 87 | 88 | 89 |
90 | Tema 91 | 92 |
93 | {#if user} 94 |
95 | {user.name} 96 | 97 |
98 | 99 | 100 | Logout 102 | {:else} 103 | 104 | {/if} 105 |
106 |
107 | -------------------------------------------------------------------------------- /src/lib/constants/price.ts: -------------------------------------------------------------------------------- 1 | type Package = { 2 | value: number; 3 | totalPrice: number; 4 | pricePerImage: number; 5 | }; 6 | 7 | export const packages: Package[] = [ 8 | { 9 | value: 10, 10 | totalPrice: 12_250, 11 | pricePerImage: 1_225 12 | }, 13 | { 14 | value: 50, 15 | totalPrice: 53_250, 16 | pricePerImage: 1_065 17 | }, 18 | { 19 | value: 100, 20 | totalPrice: 92_600, 21 | pricePerImage: 926 22 | }, 23 | { 24 | value: 200, 25 | totalPrice: 161_000, 26 | pricePerImage: 805 27 | }, 28 | { 29 | value: 500, 30 | totalPrice: 350_000, 31 | pricePerImage: 700 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /src/lib/downloads.ts: -------------------------------------------------------------------------------- 1 | import { withCatch } from '@tfkhdyt/with-catch'; 2 | import JSZip from 'jszip'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | export function downloadBlob(url: string, fileName = 'download'): void { 6 | // Create download link 7 | const link = document.createElement('a'); 8 | link.href = url; 9 | link.download = fileName; 10 | 11 | // Trigger download 12 | link.click(); 13 | } 14 | 15 | export async function downloadAll(outputs: Blob[]) { 16 | const zip = new JSZip(); 17 | 18 | for (const file of outputs) { 19 | zip.file(`remove-biji-${uuidv4()}.png`, file); // adds the image file to the zip file 20 | } 21 | 22 | const [err, zipData] = await withCatch( 23 | zip.generateAsync({ 24 | type: 'blob', 25 | streamFiles: true 26 | }) 27 | ); 28 | if (err) { 29 | console.error('Error generating zip file:', err); 30 | throw new Error('Error generating zip file', { cause: err }); 31 | } 32 | 33 | const link = document.createElement('a'); 34 | link.href = window.URL.createObjectURL(zipData); 35 | link.download = `remove-biji-batch-${uuidv4()}.zip`; 36 | link.click(); 37 | 38 | window.URL.revokeObjectURL(link.href); 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/icons/bg-replace-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/biji-icon.svelte: -------------------------------------------------------------------------------- 1 | biji 2 | -------------------------------------------------------------------------------- /src/lib/icons/bill-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/icons/buy-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/icons/circle-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/close-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/download-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/icons/download-multiple-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/icons/facebook-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/file-upload-icon.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/icons/github-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/google-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/icons/hamburger-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/icons/home-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/instagram-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/invoice-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/linkedin-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/loading-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/lib/icons/logout-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/money-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/icons/reset-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/trash-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/twitter-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/icons/view-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/icons/youtube-icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/image.ts: -------------------------------------------------------------------------------- 1 | import { withCatch } from '@tfkhdyt/with-catch'; 2 | import sharp from 'sharp'; 3 | 4 | const getOrientation = async (blob: Blob): Promise => { 5 | const buffer = await blob.arrayBuffer(); 6 | const [err, metadata] = await withCatch(sharp(Buffer.from(buffer)).metadata()); 7 | if (err) { 8 | console.error('Error reading orientation:', err); 9 | throw new Error('Error reading orientation', { cause: err }); 10 | } 11 | 12 | return metadata.orientation || 1; // Default to 1 (normal orientation) 13 | }; 14 | 15 | export const rotateImageToMatch = async (inputBlob: Blob, outputBlob: Blob): Promise => { 16 | const inputOrientation = await getOrientation(inputBlob); 17 | const outputOrientation = await getOrientation(outputBlob); 18 | 19 | if (inputOrientation === outputOrientation) { 20 | return outputBlob; // Return the same Blob if no rotation is needed 21 | } 22 | 23 | const orientationRotationMap: Record = { 24 | 1: 0, // Normal 25 | 3: 180, // Upside Down 26 | 6: 90, // Rotated 90° Clockwise 27 | 8: -90 // Rotated 90° Counterclockwise 28 | }; 29 | // Compute the relative rotation 30 | const outputRotation = orientationRotationMap[outputOrientation] || 0; 31 | const inputRotation = orientationRotationMap[inputOrientation] || 0; 32 | 33 | const rotationNeeded = inputRotation - outputRotation; 34 | 35 | // Convert Blob to Bu ffer 36 | const buffer = await outputBlob.arrayBuffer(); 37 | 38 | // Apply rotation using sharp 39 | const [err, rotatedBuffer] = await withCatch( 40 | sharp(Buffer.from(buffer)).rotate(rotationNeeded).toBuffer() 41 | ); 42 | if (err) { 43 | console.error('Error rotating image:', err); 44 | throw new Error('Error rotating image', { cause: err }); 45 | } 46 | 47 | // Convert the rotated Buffer back to a Blob 48 | const rotatedBlob = new Blob([rotatedBuffer], { type: outputBlob.type }); 49 | 50 | return rotatedBlob; 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import { RateLimiter } from 'sveltekit-rate-limiter/server'; 2 | 3 | export const guestLimiter = new RateLimiter({ 4 | IPUA: [3, 'd'] 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/remove-bg.ts: -------------------------------------------------------------------------------- 1 | import { withCatch } from '@tfkhdyt/with-catch'; 2 | import { imageSchema } from './schema/image-schema'; 3 | 4 | export const removeBg = async (images: FileList | undefined) => { 5 | if (!images) throw new Error('Tidak ada gambar yang dikirim'); 6 | 7 | const payload = imageSchema.array().safeParse(Array.from(images)); 8 | if (!payload.success) { 9 | throw payload.error; 10 | } 11 | 12 | const form = new FormData(); 13 | for (const image of payload.data) { 14 | form.append('image', image); 15 | } 16 | 17 | const [err, resp] = await withCatch( 18 | fetch('/api/remove-biji', { 19 | method: 'POST', 20 | body: form 21 | }) 22 | ); 23 | if (err) { 24 | throw err; 25 | } 26 | 27 | const [errData, data] = await withCatch(resp.json()); 28 | if (errData) { 29 | throw errData; 30 | } 31 | 32 | if (!resp.ok) { 33 | throw new Error(data.message as string); 34 | } 35 | 36 | return data as { images: string[]; creditsAmount: number | undefined }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/schema/image-schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const supportedImageTypes = ['image/jpeg', 'image/png', 'image/webp']; 4 | const maximumImageSize = 5_000_000; 5 | 6 | export const imageSchema = z 7 | .instanceof(File) 8 | .refine((file) => supportedImageTypes.includes(file.type), 'Tipe gambar tidak didukung') 9 | .refine( 10 | (file) => file.size <= maximumImageSize, 11 | 'Ukuran gambar terlalu besar, tidak boleh lebih dari 5MB' 12 | ); 13 | -------------------------------------------------------------------------------- /src/lib/schema/payment-schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const createPaymentSchema = z.object({ 4 | package: z.number() 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/server/auth.ts: -------------------------------------------------------------------------------- 1 | import { db } from '$lib/server/db'; 2 | import * as table from '$lib/server/db/schema'; 3 | import { sha256 } from '@oslojs/crypto/sha2'; 4 | import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; 5 | import type { RequestEvent } from '@sveltejs/kit'; 6 | import { withCatch } from '@tfkhdyt/with-catch'; 7 | import { eq } from 'drizzle-orm'; 8 | 9 | const DAY_IN_MS = 1000 * 60 * 60 * 24; 10 | 11 | export const sessionCookieName = 'auth-session'; 12 | 13 | export function generateSessionToken() { 14 | const bytes = crypto.getRandomValues(new Uint8Array(18)); 15 | const token = encodeBase64url(bytes); 16 | return token; 17 | } 18 | 19 | export async function createSession(token: string, userId: number) { 20 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 21 | const session: table.Session = { 22 | id: sessionId, 23 | userId, 24 | expiresAt: new Date(Date.now() + DAY_IN_MS * 30) 25 | }; 26 | 27 | const [err] = await withCatch(db.insert(table.session).values(session)); 28 | if (err) { 29 | throw new Error('Error creating session', { cause: err }); 30 | } 31 | 32 | return session; 33 | } 34 | 35 | export async function validateSessionToken(token: string) { 36 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 37 | 38 | const [result] = await db 39 | .select({ 40 | user: { 41 | id: table.user.id, 42 | name: table.user.name, 43 | email: table.user.email, 44 | picture: table.user.picture, 45 | creditsAmount: table.credits.amount 46 | }, 47 | session: table.session 48 | }) 49 | .from(table.session) 50 | .innerJoin(table.user, eq(table.session.userId, table.user.id)) 51 | .leftJoin(table.credits, eq(table.user.id, table.credits.id)) 52 | .where(eq(table.session.id, sessionId)); 53 | 54 | if (!result) { 55 | return { session: null, user: null }; 56 | } 57 | const { session, user } = result; 58 | 59 | const sessionExpired = Date.now() >= session.expiresAt.getTime(); 60 | if (sessionExpired) { 61 | const [err] = await withCatch(db.delete(table.session).where(eq(table.session.id, session.id))); 62 | if (err) { 63 | throw new Error('Error deleting session', { cause: err }); 64 | } 65 | 66 | return { session: null, user: null }; 67 | } 68 | 69 | const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; 70 | if (renewSession) { 71 | session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); 72 | 73 | const [err] = await withCatch( 74 | db 75 | .update(table.session) 76 | .set({ expiresAt: session.expiresAt }) 77 | .where(eq(table.session.id, session.id)) 78 | ); 79 | if (err) { 80 | throw new Error('Error updating session', { cause: err }); 81 | } 82 | } 83 | 84 | return { session, user }; 85 | } 86 | 87 | export type SessionValidationResult = Awaited>; 88 | 89 | export async function invalidateSession(sessionId: string) { 90 | const [err] = await withCatch(db.delete(table.session).where(eq(table.session.id, sessionId))); 91 | if (err) { 92 | throw new Error('Error invalidate session', { cause: err }); 93 | } 94 | } 95 | 96 | export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { 97 | event.cookies.set(sessionCookieName, token, { 98 | expires: expiresAt, 99 | path: '/' 100 | }); 101 | } 102 | 103 | export function deleteSessionTokenCookie(event: RequestEvent) { 104 | event.cookies.delete(sessionCookieName, { 105 | path: '/' 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/postgres-js'; 2 | import postgres from 'postgres'; 3 | import { env } from '$env/dynamic/private'; 4 | import * as schema from './schema'; 5 | 6 | if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); 7 | 8 | const client = postgres(env.DATABASE_URL); 9 | export const db = drizzle(client, { casing: 'snake_case', schema }); 10 | -------------------------------------------------------------------------------- /src/lib/server/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { relations } from 'drizzle-orm'; 2 | import { integer, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; 3 | 4 | export const user = pgTable('users', { 5 | id: serial().primaryKey(), 6 | googleId: varchar().notNull().unique(), 7 | email: varchar().notNull().unique(), 8 | name: varchar(), 9 | picture: text() 10 | // passwordHash: text('password_hash').notNull() 11 | }); 12 | 13 | export const userRelations = relations(user, ({ one, many }) => ({ 14 | creditsAmount: one(credits), 15 | sessions: many(session), 16 | invoices: many(invoices) 17 | })); 18 | 19 | export const session = pgTable('sessions', { 20 | id: text().primaryKey(), 21 | userId: integer() 22 | .notNull() 23 | .references(() => user.id), 24 | expiresAt: timestamp({ withTimezone: true, mode: 'date' }).notNull() 25 | }); 26 | 27 | export const credits = pgTable('credits', { 28 | id: integer() 29 | .primaryKey() 30 | .references(() => user.id), 31 | amount: integer().notNull().default(5) 32 | }); 33 | 34 | export const invoices = pgTable('invoices', { 35 | id: varchar().primaryKey(), 36 | userId: integer() 37 | .notNull() 38 | .references(() => user.id), 39 | status: varchar().notNull().default('UNPAID'), 40 | expiredTime: timestamp({ withTimezone: true, mode: 'date' }).notNull(), 41 | paidAt: timestamp({ withTimezone: true, mode: 'date' }), 42 | package: integer().notNull(), 43 | amount: integer().notNull(), 44 | checkoutUrl: text(), 45 | createdAt: timestamp({ withTimezone: true, mode: 'date' }).defaultNow() 46 | }); 47 | 48 | export type Session = typeof session.$inferSelect; 49 | 50 | export type User = typeof user.$inferSelect & { 51 | creditsAmount: number | null; 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/server/oauth.ts: -------------------------------------------------------------------------------- 1 | import { Google } from 'arctic'; 2 | import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, BASE_URL } from '$env/static/private'; 3 | 4 | export const google = new Google( 5 | GOOGLE_CLIENT_ID, 6 | GOOGLE_CLIENT_SECRET, 7 | `${BASE_URL}/auth/login/google/callback` 8 | ); 9 | -------------------------------------------------------------------------------- /src/lib/server/payment.ts: -------------------------------------------------------------------------------- 1 | import { packages } from '$lib/constants/price'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import crypto from 'node:crypto'; 4 | import { env } from '$env/dynamic/private'; 5 | import type { TripayInvoiceResponse } from '$lib/types/tripay'; 6 | import { withCatch } from '@tfkhdyt/with-catch'; 7 | import { db } from './db'; 8 | import * as table from '$lib/server/db/schema'; 9 | 10 | const tripayUrl = 11 | env.NODE_ENV === 'production' 12 | ? 'https://tripay.co.id/api/transaction/create' 13 | : 'https://tripay.co.id/api-sandbox/transaction/create'; 14 | const apiKey = env.TRIPAY_API_KEY; 15 | const privateKey = env.TRIPAY_PRIVATE_KEY; 16 | const merchant_code = env.TRIPAY_MERCHANT_CODE; 17 | 18 | type UserData = { 19 | id: number; 20 | name: string | null; 21 | email: string | null; 22 | }; 23 | 24 | export async function createInvoice(packageId: number, userData: UserData) { 25 | const pkg = packages.find((p) => p.value === packageId); 26 | 27 | const merchant_ref = `INV-${uuidv4()}`; 28 | const signature = crypto 29 | .createHmac('sha256', privateKey) 30 | .update(merchant_code + merchant_ref + pkg?.totalPrice) 31 | .digest('hex'); 32 | 33 | const expiry = Math.floor(Date.now() / 1000) + 60 * 60; // 1 jam 34 | 35 | const payload = { 36 | method: 'QRIS2', 37 | merchant_ref, 38 | amount: pkg?.totalPrice, 39 | customer_name: userData.name, 40 | customer_email: userData.email, 41 | // customer_phone: '081234567890', 42 | order_items: [ 43 | { 44 | sku: 'BIJI', 45 | name: `Saldo Remove Biji`, 46 | price: pkg?.pricePerImage, 47 | quantity: pkg?.value 48 | // product_url: 'https://tokokamu.com/product/nama-produk-1', 49 | // image_url: 'https://tokokamu.com/product/nama-produk-1.jpg' 50 | } 51 | ], 52 | return_url: `${env.BASE_URL}/invoices`, 53 | callback_url: `${env.BASE_URL}/api/payment/callback`, 54 | expired_time: expiry, 55 | signature 56 | }; 57 | 58 | const [err, response] = await withCatch( 59 | fetch(tripayUrl, { 60 | method: 'POST', 61 | body: JSON.stringify(payload), 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | Authorization: 'Bearer ' + apiKey 65 | } 66 | }) 67 | ); 68 | if (err) { 69 | throw new Error('Error creating payment', { cause: err }); 70 | } 71 | 72 | const data = await response.json(); 73 | if (!response.ok) { 74 | throw new Error(data.message); 75 | } 76 | 77 | const invoice: TripayInvoiceResponse = data.data; 78 | 79 | const [errInsert] = await withCatch( 80 | db.insert(table.invoices).values({ 81 | id: invoice.merchant_ref, 82 | userId: userData.id, 83 | status: invoice.status, 84 | expiredTime: new Date(invoice.expired_time * 1000), 85 | package: packageId, 86 | amount: invoice.amount, 87 | checkoutUrl: invoice.checkout_url 88 | }) 89 | ); 90 | if (errInsert) { 91 | throw new Error('Error creating invoice', { cause: errInsert }); 92 | } 93 | 94 | return invoice.checkout_url; 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/server/user.ts: -------------------------------------------------------------------------------- 1 | import { withCatch } from '@tfkhdyt/with-catch'; 2 | import { eq } from 'drizzle-orm'; 3 | import { db } from './db'; 4 | import * as table from './db/schema'; 5 | 6 | export async function createUser(googleId: string, email: string, name: string, picture: string) { 7 | const [user] = await db 8 | .insert(table.user) 9 | .values({ 10 | googleId, 11 | email, 12 | name, 13 | picture 14 | }) 15 | .returning(); 16 | if (!user) { 17 | throw new Error('Failed to create user'); 18 | } 19 | 20 | const [err] = await withCatch(db.insert(table.credits).values({ id: user.id, amount: 5 })); 21 | if (err) { 22 | throw new Error('Failed to create credits', { cause: err }); 23 | } 24 | 25 | return user; 26 | } 27 | 28 | export async function getUserFromGoogleId(googleId: string) { 29 | const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId)); 30 | if (!user) { 31 | return null; 32 | } 33 | 34 | return user; 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/stores/credit.svelte.ts: -------------------------------------------------------------------------------- 1 | let amount = $state(null); 2 | 3 | export function getCreditsStore() { 4 | function setAmount(newAmount: number) { 5 | amount = newAmount; 6 | } 7 | 8 | return { 9 | get amount() { 10 | return amount; 11 | }, 12 | setAmount 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/types/tripay.ts: -------------------------------------------------------------------------------- 1 | export type TripayInvoiceResponse = { 2 | reference: string; 3 | merchant_ref: string; 4 | payment_selection_type: string; 5 | payment_method: string; 6 | payment_name: string; 7 | customer_name: string; 8 | customer_email: string; 9 | customer_phone: null; 10 | callback_url: string; 11 | return_url: string; 12 | amount: number; 13 | fee_merchant: number; 14 | fee_customer: number; 15 | total_fee: number; 16 | amount_received: number; 17 | pay_code: null; 18 | pay_url: null; 19 | checkout_url: string; 20 | status: string; 21 | expired_time: number; 22 | qr_string: string; 23 | qr_url: string; 24 | }; 25 | 26 | export type TripayCallbackResponse = { 27 | reference: string; 28 | merchant_ref: string; 29 | payment_method: string; 30 | payment_method_code: string; 31 | total_amount: number; 32 | fee_merchant: number; 33 | fee_customer: number; 34 | total_fee: number; 35 | amount_received: number; 36 | is_closed_payment: number; 37 | status: string; 38 | paid_at: number; 39 | note: null; 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | export const removeFileFromList = (fileToRemove: File, images: FileList) => { 3 | // Convert FileList to Array 4 | const filesArray: File[] = Array.from(images); 5 | 6 | // Find index of file to remove 7 | const index: number = filesArray.findIndex((file) => file === fileToRemove); 8 | 9 | // Remove the file if found 10 | if (index > -1) { 11 | filesArray.splice(index, 1); 12 | } 13 | 14 | // Create a new DataTransfer object 15 | const dt: DataTransfer = new DataTransfer(); 16 | 17 | // Add remaining files to DataTransfer object 18 | filesArray.forEach((file) => dt.items.add(file)); 19 | 20 | // Return new FileList 21 | return dt.files; 22 | }; 23 | 24 | export function base64ToBlob(base64: string) { 25 | const byteCharacters = atob(base64); 26 | const byteArrays = []; 27 | 28 | for (let i = 0; i < byteCharacters.length; i++) { 29 | byteArrays.push(byteCharacters.charCodeAt(i)); 30 | } 31 | 32 | const byteArray = new Uint8Array(byteArrays); 33 | return new Blob([byteArray], { type: 'image/png' }); 34 | } 35 | 36 | const formatter = new Intl.NumberFormat('id-ID', { 37 | style: 'currency', 38 | currency: 'IDR', 39 | trailingZeroDisplay: 'stripIfInteger' 40 | }); 41 | 42 | export function formatRupiah(number: number) { 43 | return formatter.format(number); 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/(app)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { LayoutServerLoad } from './$types'; 2 | 3 | export const load: LayoutServerLoad = async ({ parent }) => { 4 | const data = await parent(); 5 | 6 | return data; 7 | }; 8 | -------------------------------------------------------------------------------- /src/routes/(app)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |
18 |
19 | {@render children()} 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/routes/(app)/app/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | 3 | export const load: PageServerLoad = async ({ parent }) => { 4 | const data = await parent(); 5 | return data; 6 | }; 7 | -------------------------------------------------------------------------------- /src/routes/(app)/app/+page.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | {isLoading ? '[Processing...] ' : ''}Remove Biji - Hilangkan bijimu dengan mudah 27 | 28 | 29 |
30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /src/routes/(app)/invoices/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | import { db } from '$lib/server/db'; 4 | import * as table from '$lib/server/db/schema'; 5 | import { desc, eq } from 'drizzle-orm'; 6 | import { withCatch } from '@tfkhdyt/with-catch'; 7 | 8 | export const load: PageServerLoad = async ({ parent }) => { 9 | const data = await parent(); 10 | if (!data.user) { 11 | return redirect(302, '/'); 12 | } 13 | 14 | const [err, invoices] = await withCatch( 15 | db 16 | .select() 17 | .from(table.invoices) 18 | .where(eq(table.invoices.userId, data.user.id)) 19 | .orderBy(desc(table.invoices.createdAt)) 20 | ); 21 | if (err) { 22 | throw new Error('Error loading invoices', { cause: err }); 23 | } 24 | 25 | return { user: data.user, invoices }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/(app)/invoices/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | {title} 23 | 24 | 25 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {#each data.invoices as invoice (invoice.id)} 61 | 62 | 66 | 67 | 76 | 80 | 96 | 97 | {/each} 98 | 99 |
PaketTotal HargaStatusTanggal DibuatTanggal Dibayar
63 | 64 | {invoice.package}{formatRupiah(invoice.amount)} 68 | {#if invoice.status === 'PAID'} 69 | {invoice.status} 70 | {:else if invoice.status === 'UNPAID'} 71 | {invoice.status} 72 | {:else} 73 | {invoice.status} 74 | {/if} 75 | {!!invoice.createdAt && 78 | format(invoice.createdAt, 'dd MMM yyyy, HH:mm', { locale: id })} 81 | {#if invoice.paidAt} 82 | 83 | {format(invoice.paidAt, 'dd MMM yyyy, HH:mm', { locale: id })} 84 | 85 | {:else if invoice.status === 'UNPAID'} 86 |
87 | 88 | 89 | Bayar 91 |
92 | {:else} 93 | - 94 | {/if} 95 |
100 |
101 | -------------------------------------------------------------------------------- /src/routes/(app)/pricing/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | {title} 14 | 15 | 16 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | {#each packages as row} 58 | 59 | 63 | 64 | 65 | 66 | {/each} 67 | 68 | 74 |
Jumlah biji*Harga**Harga per biji
51 | 52 | 3 / hari*** 53 | GratisGratis
60 | 61 | {row.value} 62 | {formatRupiah(row.totalPrice)}{formatRupiah(row.pricePerImage)} / biji
75 |
76 | 77 |
78 |

79 | *1 80 | = 1 gambar 81 |

82 |

83 | **Belum termasuk biaya admin 84 |

85 |

***Non-akumulatif

86 |
87 | -------------------------------------------------------------------------------- /src/routes/(app)/privacy-policy/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | Privacy Policy - Remove Biji 11 | 12 | 13 | 31 | 32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /src/routes/(app)/topup/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { createPaymentSchema } from '$lib/schema/payment-schema'; 2 | import { createInvoice } from '$lib/server/payment'; 3 | import { redirect } from '@sveltejs/kit'; 4 | import { withCatch } from '@tfkhdyt/with-catch'; 5 | import { fail, message, superValidate } from 'sveltekit-superforms'; 6 | import { zod } from 'sveltekit-superforms/adapters'; 7 | import type { PageServerLoad } from './$types'; 8 | 9 | export const load: PageServerLoad = async ({ parent }) => { 10 | const data = await parent(); 11 | if (!data.user) { 12 | return redirect(302, '/auth/login/google'); 13 | } 14 | 15 | const form = await superValidate(zod(createPaymentSchema)); 16 | 17 | return { data, form }; 18 | }; 19 | 20 | export const actions = { 21 | default: async ({ locals, request }) => { 22 | const form = await superValidate(request, zod(createPaymentSchema)); 23 | if (!form.valid) { 24 | return fail(400, { form }); 25 | } 26 | 27 | if (!locals.user) { 28 | return message(form, { type: 'error', message: 'Kamu harus login terlebih dahulu' }); 29 | } 30 | 31 | const [err, checkoutUrl] = await withCatch( 32 | createInvoice(form.data.package, { 33 | id: locals.user.id, 34 | name: locals.user.name, 35 | email: locals.user.email 36 | }) 37 | ); 38 | if (err) { 39 | return message(form, { type: 'error', message: err.message }); 40 | } 41 | 42 | return redirect(302, checkoutUrl); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/routes/(app)/topup/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | {title} 32 | 33 | 34 | 52 | 53 | 54 | 55 |
56 |
57 |
58 | {#each packages as row, i} 59 | 81 | {/each} 82 |
83 |
84 | 85 |
86 |

87 | 1 88 | = 1 gambar 89 |

90 | 91 | 99 |
100 |
101 | -------------------------------------------------------------------------------- /src/routes/(landing-page)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | {@render children()} 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/routes/(landing-page)/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load: PageServerLoad = async ({ parent }) => { 5 | const data = await parent(); 6 | if (data.user) { 7 | return redirect(301, '/app'); 8 | } 9 | 10 | return {}; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/(landing-page)/+page.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | Remove Biji - Hilangkan bijimu dengan mudah 71 | 72 | 73 |
74 |
75 |
76 |

80 | Hapus Background Gambar 81 | dalam Sekejap 82 |

83 |

84 | Solusi AI yang powerful untuk menghapus background foto Anda secara otomatis dalam hitungan 85 | detik 86 |

87 | Coba Gratis 88 |
89 |
90 | 91 |
94 |
95 | 107 |
108 |
109 | {#each features as feature} 110 |
113 | 114 |

{feature.title}

115 |

{feature.description}

116 |
117 | {/each} 118 |
119 |
120 | 121 |
122 |
123 |

Cara Kerja

124 |
125 | {#each caraKerja as cara, i} 126 |
129 |
132 | 133 |
134 |

{i + 1}. {cara.title}

135 |

{cara.description}

136 |
137 | {/each} 138 |
139 |
140 |
141 | 142 |
143 |
144 |

Frequently Asked Questions

145 | 146 | {#each faq as f} 147 | 148 | 149 | {f.question} 150 | 151 | 152 | 153 | {f.answer} 154 | 155 | 156 | 157 | {/each} 158 | 159 |
160 |
161 | 162 |
163 |

Siap Mencoba?

164 |

165 | Dapatkan 5 saldo gratis untuk member baru 166 |

167 | Coba Sekarang 168 |
169 |
170 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

{$page.status}

8 |

{$page.error?.message}

9 | 10 | 11 | 12 | Kembali ke halaman utama 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { LayoutServerLoad } from './$types'; 2 | 3 | export const load: LayoutServerLoad = async ({ locals }) => { 4 | return { 5 | user: locals.user 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 38 | 39 | 40 | 58 | 59 | {@render children()} 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/routes/api/payment/callback/+server.ts: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private'; 2 | import { error, json } from '@sveltejs/kit'; 3 | import type { RequestHandler } from './$types'; 4 | import crypto from 'node:crypto'; 5 | import { db } from '$lib/server/db'; 6 | import * as table from '$lib/server/db/schema'; 7 | import type { TripayCallbackResponse } from '$lib/types/tripay'; 8 | import { eq, sql } from 'drizzle-orm'; 9 | import { withCatch } from '@tfkhdyt/with-catch'; 10 | 11 | export const POST: RequestHandler = async ({ request }) => { 12 | const data: TripayCallbackResponse = await request.json(); 13 | const signature = crypto 14 | .createHmac('sha256', env.TRIPAY_PRIVATE_KEY) 15 | .update(JSON.stringify(data)) 16 | .digest('hex'); 17 | const signatureFromTripay = request.headers.get('X-Callback-Signature'); 18 | 19 | if (signature !== signatureFromTripay) { 20 | return error(401, 'Signature tidak valid'); 21 | } 22 | 23 | await db.transaction(async (trx) => { 24 | const [invoice] = await trx 25 | .update(table.invoices) 26 | .set({ 27 | status: data.status, 28 | paidAt: data.status === 'PAID' ? new Date(data.paid_at * 1000) : undefined 29 | }) 30 | .where(eq(table.invoices.id, data.merchant_ref)) 31 | .returning({ package: table.invoices.package, userId: table.invoices.userId }); 32 | 33 | if (data.status === 'PAID') { 34 | const [err] = await withCatch( 35 | trx 36 | .update(table.credits) 37 | .set({ 38 | amount: sql`${table.credits.amount} + ${invoice.package}` 39 | }) 40 | .where(eq(table.credits.id, invoice.userId)) 41 | ); 42 | if (err) { 43 | throw new Error('Error updating credits', { cause: err }); 44 | } 45 | } 46 | }); 47 | 48 | return json({ success: true }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/routes/api/remove-biji/+server.ts: -------------------------------------------------------------------------------- 1 | import { guestLimiter } from '$lib/rate-limiter'; 2 | import { error } from '@sveltejs/kit'; 3 | import type { RequestHandler } from './$types'; 4 | import { removeBackground } from '@imgly/background-removal-node'; 5 | import { env } from '$env/dynamic/private'; 6 | import { rotateImageToMatch } from '$lib/image'; 7 | import { withCatch } from '@tfkhdyt/with-catch'; 8 | import { db } from '$lib/server/db'; 9 | import * as table from '$lib/server/db/schema'; 10 | import { eq, sql } from 'drizzle-orm'; 11 | 12 | export const POST: RequestHandler = async (event) => { 13 | const { request, locals } = event; 14 | 15 | const formData = await request.formData(); 16 | const files = formData.getAll('image'); 17 | 18 | const isLimited = await guestLimiter.isLimited(event); 19 | 20 | if (env.NODE_ENV === 'production') { 21 | if (!locals.user && isLimited) { 22 | throw error(429, 'Kuota free tier-mu sudah habis, daftar untuk mendapatkan 5 saldo gratis'); 23 | } 24 | 25 | if (locals.user?.creditsAmount === 0 && isLimited) { 26 | throw error( 27 | 429, 28 | 'Kamu hanya mendapatkan kuota free tier 3 kali per hari, silakan topup terlebih dahulu' 29 | ); 30 | } 31 | } 32 | 33 | if (locals.user?.creditsAmount && files.length > locals.user.creditsAmount) { 34 | throw error(429, 'Saldomu tidak mencukupi, silakan topup terlebih dahulu'); 35 | } 36 | 37 | const result = await Promise.allSettled( 38 | files.map(async (image) => { 39 | const [err, output] = await withCatch(removeBackground(image)); // Process the image 40 | if (err) { 41 | console.error('Error removing background:', err); 42 | throw new Error('Error removing background', { cause: err }); 43 | } 44 | const rotatedOutput = await rotateImageToMatch( 45 | new Blob([image], { type: 'image/png' }), 46 | output! 47 | ); 48 | 49 | const base64 = await blobToBase64(rotatedOutput); // Convert the blob to base64 50 | return base64; 51 | }) 52 | ); 53 | 54 | const output: string[] = []; 55 | for (const item of result) { 56 | if (item.status === 'fulfilled') { 57 | output.push(item.value); 58 | } 59 | } 60 | 61 | let creditsAmount = undefined; 62 | if (locals.user && locals.user.creditsAmount && locals.user.creditsAmount > 0) { 63 | const [row] = await db 64 | .update(table.credits) 65 | .set({ 66 | amount: sql`${table.credits.amount} - ${files.length}` 67 | }) 68 | .where(eq(table.credits.id, locals.user.id)) 69 | .returning({ amount: table.credits.amount }); 70 | if (!row) { 71 | throw new Error('Error updating credits'); 72 | } 73 | 74 | creditsAmount = row.amount; 75 | } 76 | 77 | return new Response(JSON.stringify({ images: output, creditsAmount }), { 78 | headers: { 'Content-Type': 'application/json' } 79 | }); 80 | }; 81 | 82 | async function blobToBase64(blob: Blob) { 83 | const arrayBuffer = await blob.arrayBuffer(); 84 | const base64 = Buffer.from(arrayBuffer).toString('base64'); 85 | return base64; 86 | } 87 | -------------------------------------------------------------------------------- /src/routes/auth/login/google/+server.ts: -------------------------------------------------------------------------------- 1 | import { google } from '$lib/server/oauth'; 2 | import { generateCodeVerifier, generateState } from 'arctic'; 3 | 4 | import type { RequestEvent } from './$types'; 5 | 6 | export function GET(event: RequestEvent): Response { 7 | const state = generateState(); 8 | const codeVerifier = generateCodeVerifier(); 9 | const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']); 10 | 11 | event.cookies.set('google_oauth_state', state, { 12 | httpOnly: true, 13 | maxAge: 60 * 10, 14 | secure: import.meta.env.PROD, 15 | path: '/', 16 | sameSite: 'lax' 17 | }); 18 | event.cookies.set('google_code_verifier', codeVerifier, { 19 | httpOnly: true, 20 | maxAge: 60 * 10, 21 | secure: import.meta.env.PROD, 22 | path: '/', 23 | sameSite: 'lax' 24 | }); 25 | 26 | return new Response(null, { 27 | status: 302, 28 | headers: { 29 | Location: url.toString() 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/routes/auth/login/google/callback/+server.ts: -------------------------------------------------------------------------------- 1 | import { google } from '$lib/server/oauth'; 2 | import { ObjectParser } from '@pilcrowjs/object-parser'; 3 | import { createUser, getUserFromGoogleId } from '$lib/server/user'; 4 | import { createSession, generateSessionToken, setSessionTokenCookie } from '$lib/server/auth'; 5 | import { decodeIdToken } from 'arctic'; 6 | 7 | import type { RequestEvent } from './$types'; 8 | import { withCatch } from '@tfkhdyt/with-catch'; 9 | 10 | export async function GET(event: RequestEvent): Promise { 11 | const storedState = event.cookies.get('google_oauth_state') ?? null; 12 | const codeVerifier = event.cookies.get('google_code_verifier') ?? null; 13 | const code = event.url.searchParams.get('code'); 14 | const state = event.url.searchParams.get('state'); 15 | 16 | if (storedState === null || codeVerifier === null || code === null || state === null) { 17 | return new Response('Please restart the process.', { 18 | status: 400 19 | }); 20 | } 21 | if (storedState !== state) { 22 | return new Response('Please restart the process.', { 23 | status: 400 24 | }); 25 | } 26 | 27 | const [err, tokens] = await withCatch(google.validateAuthorizationCode(code, codeVerifier)); 28 | if (err) { 29 | return new Response('Please restart the process.', { 30 | status: 400 31 | }); 32 | } 33 | 34 | const claims = decodeIdToken(tokens.idToken()); 35 | const claimsParser = new ObjectParser(claims); 36 | 37 | const googleId = claimsParser.getString('sub'); 38 | const name = claimsParser.getString('name'); 39 | const picture = claimsParser.getString('picture'); 40 | const email = claimsParser.getString('email'); 41 | 42 | const existingUser = await getUserFromGoogleId(googleId); 43 | if (existingUser !== null) { 44 | const sessionToken = generateSessionToken(); 45 | const session = await createSession(sessionToken, existingUser.id); 46 | setSessionTokenCookie(event, sessionToken, session.expiresAt); 47 | return new Response(null, { 48 | status: 302, 49 | headers: { 50 | Location: '/' 51 | } 52 | }); 53 | } 54 | 55 | const user = await createUser(googleId, email, name, picture); 56 | const sessionToken = generateSessionToken(); 57 | const session = await createSession(sessionToken, user.id); 58 | setSessionTokenCookie(event, sessionToken, session.expiresAt); 59 | return new Response(null, { 60 | status: 302, 61 | headers: { 62 | Location: '/' 63 | } 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/routes/auth/logout/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/auth'; 4 | 5 | export const GET: RequestHandler = async (event) => { 6 | const { locals } = event; 7 | 8 | if (!locals.session) { 9 | return error(401); 10 | } 11 | await invalidateSession(locals.session.id); 12 | deleteSessionTokenCookie(event); 13 | 14 | return redirect(301, '/'); 15 | }; 16 | -------------------------------------------------------------------------------- /static/after.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/after.webp -------------------------------------------------------------------------------- /static/before.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/before.webp -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/favicon.ico -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { mdsvex } from 'mdsvex'; 2 | import adapter from '@sveltejs/adapter-node'; 3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://svelte.dev/docs/kit/integrations 8 | // for more information about preprocessors 9 | preprocess: [vitePreprocess(), mdsvex()], 10 | 11 | kit: { 12 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 13 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 14 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 15 | adapter: adapter() 16 | }, 17 | 18 | extensions: ['.svelte', '.svx'] 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { skeleton } from '@skeletonlabs/tw-plugin'; 2 | import { join } from 'path'; 3 | import type { Config } from 'tailwindcss'; 4 | import typography from '@tailwindcss/typography'; 5 | 6 | export default { 7 | darkMode: 'selector', 8 | content: [ 9 | './src/**/*.{html,js,svelte,ts,svx}', 10 | join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}') 11 | ], 12 | 13 | theme: { 14 | extend: {} 15 | }, 16 | 17 | plugins: [ 18 | skeleton({ 19 | themes: { preset: ['skeleton'] } 20 | }), 21 | typography() 22 | ] 23 | } satisfies Config; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | --------------------------------------------------------------------------------