initial commit

This commit is contained in:
fiatcode 2025-10-18 10:37:22 +07:00
commit 83337e0574
14 changed files with 2825 additions and 0 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
# Example environment variables
NODE_ENV=development
UNSPLASH_ACCESS_KEY=your_unsplash_access_key_here
PORT=8080

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# Node.js
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# TypeScript
*.tsbuildinfo
# Logs
logs/
*.log
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.local
.env.*.local
# Build output
dist/
build/
# Misc
*.swp
*.swo
# Coverage
coverage/
# Others
.idea/
*.iml
# Temporary files
.tmp/
.temp/
# db
*.db

9
.prettierignore Normal file
View file

@ -0,0 +1,9 @@
node_modules/
dist/
build/
coverage/
.vscode/
.idea/
*.log
*.tsbuildinfo
*.db

9
.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always"
}

6
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

18
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Run API Server (npm start)",
"runtimeExecutable": "npm",
"runtimeArgs": [
"start"
],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

2513
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"type": "module",
"name": "kuwot-api",
"version": "1.0.0",
"description": "Kuwot app API powered by ExpressJs",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "tsc && node dist/index.js",
"format": "prettier --write ."
},
"author": "Dhemas Nurjaya",
"license": "ISC",
"dependencies": {
"dotenv": "^17.2.3",
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.8.1",
"eslint": "^9.38.0",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
}

45
src/data/db.ts Normal file
View file

@ -0,0 +1,45 @@
import { DatabaseSync } from 'node:sqlite';
import type { Quote, Translation } from '../types.js';
const MAX_QUOTE_ID = 250000;
const db = new DatabaseSync('quotes.db');
function getRandomQuote(tableName: string = 'quotes'): Quote {
const randomId = Math.floor(Math.random() * MAX_QUOTE_ID) + 1;
const row = db
.prepare(`SELECT * FROM ${tableName} WHERE id = ?`)
.get(randomId);
if (!row) {
throw new Error(`Quote with id ${randomId} not found.`);
}
return {
id: row['id'] as number,
text: row['quote'] as string,
author: row['author'] as string,
};
}
function getQuote(id: number, tableName: string = 'quotes'): Quote {
const row = db.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).get(id);
if (!row) {
throw new Error(`Quote with id ${id} not found.`);
}
return {
id: row['id'] as number,
text: row['quote'] as string,
author: row['author'] as string,
};
}
function listTranslations(): Translation[] {
const rows = db.prepare(`SELECT * FROM translations`).all();
return rows.map((row) => {
return {
id: row['id'] as number,
lang: row['lang'] as string,
tableName: row['table_name'] as string,
};
});
}
export { getQuote, getRandomQuote, listTranslations };

38
src/data/unsplash.ts Normal file
View file

@ -0,0 +1,38 @@
import type { UnsplashImage } from '../types';
async function listRandomImages(count: number = 10): Promise<UnsplashImage[]> {
const headers = {
Authorization: `Client-ID ${process.env.UNSPLASH_ACCESS_KEY}`,
'Accept-Version': 'v1',
};
const response = await fetch(
`https://api.unsplash.com/photos/random?count=${count}`,
{ headers }
);
const data = await response.json();
const images: UnsplashImage[] = Array.isArray(data)
? data.map((img: any) => ({
id: img.id,
description: img.alt_description ?? 'No description',
color: img.color,
blurHash: img.blur_hash,
url: img.urls.regular,
originUrl: buildUtmUrl(img.links.html),
authorName: img.user.name,
authorBio: img.user.bio ?? 'No bio',
authorLocation: img.user.location,
authorTotalLikes: img.user.total_likes,
authorTotalPhotos: img.user.total_photos,
authorIsForHire: img.user.for_hire,
authorProfileImageUrl: img.user.profile_image.large,
authorUrl: buildUtmUrl(img.user.links.html),
}))
: [];
return images;
}
function buildUtmUrl(url: string): string {
return `${url}?utm_source=kuwot-api&utm_medium=referral`;
}
export { listRandomImages };

20
src/index.ts Normal file
View file

@ -0,0 +1,20 @@
import 'dotenv/config';
import express from 'express';
import {
onGetImages,
onGetQuoteById,
onGetRandomQuote,
onGetTranslations,
} from './routes.js';
const app = express();
app.get('/quotes/random', onGetRandomQuote);
app.get('/quotes/:id', onGetQuoteById);
app.get('/translations', onGetTranslations);
app.get('/images', onGetImages);
const port = process.env.PORT ? Number(process.env.PORT) : 8080;
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});

48
src/routes.ts Normal file
View file

@ -0,0 +1,48 @@
import type { Request, Response } from 'express';
import { getQuote, getRandomQuote, listTranslations } from './data/db.js';
import { listRandomImages } from './data/unsplash.js';
function onGetRandomQuote(_req: Request, res: Response): void {
try {
const quote = getRandomQuote();
res.status(200).send(quote);
} catch (error: any) {
res.status(500).send({ error: error.message });
}
}
function onGetQuoteById(req: Request, res: Response): void {
const idParam = req.params['id'];
if (!idParam) {
res.status(400).send({ error: 'Quote ID is required.' });
return;
}
try {
const id = parseInt(idParam);
const quote = getQuote(id);
res.status(200).send(quote);
} catch (error: any) {
res.status(500).send({ error: error.message });
}
}
function onGetTranslations(_req: Request, res: Response): void {
try {
const translations = listTranslations();
res.status(200).send(translations);
} catch (error: any) {
res.status(500).send({ error: error.message });
}
}
async function onGetImages(_req: Request, res: Response): Promise<void> {
try {
const images = await listRandomImages();
res.status(200).send(images);
} catch (error: any) {
res.status(500).send({ error: error.message });
}
}
export { onGetImages, onGetQuoteById, onGetRandomQuote, onGetTranslations };

28
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,28 @@
export interface Quote {
id: number;
text: string;
author: string;
}
export interface Translation {
id: number;
lang: string;
tableName: string;
}
export interface UnsplashImage {
id: string;
description: string;
color: string;
blurHash: string;
url: string;
originUrl: string;
authorName: string;
authorBio: string;
authorLocation: string;
authorTotalLikes: number;
authorTotalPhotos: number;
authorIsForHire: boolean;
authorProfileImageUrl: string;
authorUrl: string;
}

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "Node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}