diff --git a/.gitignore b/.gitignore index 9a5aced..462d067 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* +.vercel diff --git a/api/collage.js b/api/collage.js index e69de29..ac7a113 100644 --- a/api/collage.js +++ b/api/collage.js @@ -0,0 +1,3 @@ +export default function handler(req, res) { + res.status(200).send('API is running'); +} \ No newline at end of file diff --git a/api/frames.js b/api/frames.js index e69de29..257ecc6 100644 --- a/api/frames.js +++ b/api/frames.js @@ -0,0 +1,62 @@ +import axios from 'axios'; +import sharp from 'sharp'; +import fs from 'fs'; +import path from 'path'; + +export default async function handler(req, res) { + try { + // Get username from query parameter + const username = req.query.username; + const theme = req.query.theme || 'base'; + const size = Math.max(64, Math.min(Number(req.query.size || 256), 1024)); + + // Validate username + if (!username || typeof username !== 'string' || username.trim() === '') { + return res.status(400).json({ error: 'Username is required' }); + } + + console.log(`Fetching avatar for username=${username}, theme=${theme}, size=${size}`); + + // Fetch GitHub avatar + const avatarUrl = `https://github.com/${username}.png?size=${size}`; + let avatarResponse; + + try { + avatarResponse = await axios.get(avatarUrl, { + responseType: 'arraybuffer', + timeout: 10000, + validateStatus: (status) => status === 200 + }); + } catch (axiosError) { + if (axiosError.response?.status === 404) { + return res.status(404).json({ error: 'GitHub user not found' }); + } + throw axiosError; + } + + const avatarBuffer = Buffer.from(avatarResponse.data); + + // Locate theme frame + const themePath = path.join(process.cwd(), 'public', 'frames', theme, 'frame.png'); + if (!fs.existsSync(themePath)) { + return res.status(404).json({ error: `Theme '${theme}' not found` }); + } + const frameBuffer = fs.readFileSync(themePath); + + // Resize and overlay + const avatarResized = await sharp(avatarBuffer).resize(size, size).png().toBuffer(); + const frameResized = await sharp(frameBuffer).resize(size, size).png().toBuffer(); + + const finalImage = await sharp(avatarResized) + .composite([{ input: frameResized, gravity: 'center' }]) + .png() + .toBuffer(); + + res.setHeader('Content-Type', 'image/png'); + res.setHeader('Cache-Control', 'public, max-age=3600'); + res.send(finalImage); + } catch (error) { + console.error('Error processing avatar:', error); + res.status(500).json({ error: 'Something went wrong.' }); + } +} \ No newline at end of file diff --git a/api/themes.js b/api/themes.js index e69de29..2934347 100644 --- a/api/themes.js +++ b/api/themes.js @@ -0,0 +1,37 @@ +import fs from 'fs'; +import path from 'path'; + +export default function handler(req, res) { + try { + const framesDir = path.join(process.cwd(), 'public', 'frames'); + + if (!fs.existsSync(framesDir)) { + return res.status(500).json({ error: "Frames directory not found" }); + } + + const themes = fs.readdirSync(framesDir).filter(folder => + fs.existsSync(path.join(framesDir, folder, 'frame.png')) + ); + + const result = themes.map(theme => { + const metadataPath = path.join(framesDir, theme, 'metadata.json'); + let metadata = { name: theme, description: `${theme} frame theme` }; + + if (fs.existsSync(metadataPath)) { + try { + const fileContent = fs.readFileSync(metadataPath, 'utf-8'); + metadata = { ...metadata, ...JSON.parse(fileContent) }; + } catch (parseError) { + console.warn(`Invalid metadata.json for theme ${theme}`); + } + } + + return { theme, ...metadata }; + }); + + res.json(result); + } catch (error) { + console.error('Error listing themes:', error); + res.status(500).json({ error: "Failed to list themes" }); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e881e0..e5ccdc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/express": "^4.17.23", - "@types/node": "^20.19.17", + "@types/node": "^24.5.2", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.9.2" @@ -522,13 +522,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", - "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/qs": { @@ -2276,9 +2276,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 68351f4..44b9768 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/express": "^4.17.23", - "@types/node": "^20.19.17", + "@types/node": "^24.5.2", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.9.2"