initial commit
This commit is contained in:
commit
83337e0574
14 changed files with 2825 additions and 0 deletions
4
.env.example
Normal file
4
.env.example
Normal 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
43
.gitignore
vendored
Normal 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
9
.prettierignore
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
*.db
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal 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
6
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal 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
2513
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
Normal file
28
package.json
Normal 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
45
src/data/db.ts
Normal 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
38
src/data/unsplash.ts
Normal 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
20
src/index.ts
Normal 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
48
src/routes.ts
Normal 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
28
src/types.d.ts
vendored
Normal 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
16
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue