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