Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions api/duolingo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Client } from '@neondatabase/serverless';

export const config = {
runtime: 'edge',
};

export default async function (req, event) {
const url = new URL(req.url);

// Handle CORS
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}

if (!process.env.NETLIFY_DATABASE_URL) {
console.error("NETLIFY_DATABASE_URL not set");
return new Response(JSON.stringify({ error: "Server configuration error" }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}

const client = new Client(process.env.NETLIFY_DATABASE_URL);

try {
await client.connect();

if (req.method === 'GET') {
// Create table if not exists (lazy initialization for simplicity in this demo)
// Note: In production, use migrations.
await client.query(`
CREATE TABLE IF NOT EXISTS duolingo_stats (
id SERIAL PRIMARY KEY,
date DATE NOT NULL DEFAULT CURRENT_DATE,
streak_count INTEGER NOT NULL,
accuracy_percentage INTEGER NOT NULL,
words_learned TEXT,
leaderboard_position INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
`);

const { rows } = await client.query('SELECT * FROM duolingo_stats ORDER BY date DESC, created_at DESC');
event.waitUntil(client.end());

return new Response(JSON.stringify(rows), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}

if (req.method === 'POST') {
const body = await req.json();
const { date, streak_count, accuracy_percentage, words_learned, leaderboard_position } = body;

if (streak_count === undefined || accuracy_percentage === undefined) {
return new Response(JSON.stringify({ error: "Missing required fields" }), {
status: 400,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}

const dateVal = date || new Date().toISOString().split('T')[0];

const query = `
INSERT INTO duolingo_stats (date, streak_count, accuracy_percentage, words_learned, leaderboard_position)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
`;
const values = [dateVal, streak_count, accuracy_percentage, words_learned, leaderboard_position];

const { rows } = await client.query(query, values);

event.waitUntil(client.end());

return new Response(JSON.stringify(rows[0]), {
status: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}

return new Response("Method not allowed", { status: 405 });

} catch (error) {
console.error("Database error:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
122 changes: 122 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@ampproject/toolbox-optimizer": "^2.9.0",
"@neondatabase/serverless": "^1.0.2",
"any-shell-escape": "^0.1.1",
"clean-css": "^4.2.3",
"concurrently": "^7.0.0",
Expand Down
Loading