Skip to content

Commit 4211066

Browse files
committed
Chore: initial
1 parent e4af2af commit 4211066

14 files changed

Lines changed: 1013 additions & 82 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# misc
2424
.DS_Store
2525
*.pem
26+
data/reminders.json
2627

2728
# debug
2829
npm-debug.log*

CLAUDE.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
npm run dev # Start development server at localhost:3000
9+
npm run build # Production build
10+
npm run lint # Run ESLint
11+
```
12+
13+
## Environment Variables
14+
15+
Required in `.env.local`:
16+
- `RESEND_API_KEY` - API key from Resend
17+
- `RESEND_FROM` - Verified sender email in Resend
18+
19+
Optional:
20+
- `GITHUB_TOKEN` - Avoids rate limits and enables private repo access
21+
22+
## Architecture
23+
24+
Next.js 16 app with App Router. Single-page UI with REST API endpoints.
25+
26+
**Data Flow:**
27+
1. User adds reminder (repo, email, threshold) via UI
28+
2. Check runs (manual or cron) → fetches GitHub API → compares push date
29+
3. If stale (no push within threshold days) and not recently notified → sends email via Resend
30+
31+
**Key Files:**
32+
- `src/lib/checker.ts` - Core logic: iterates reminders, checks staleness, triggers emails
33+
- `src/lib/github.ts` - GitHub API client for repo push status
34+
- `src/lib/mailer.ts` - Resend email wrapper
35+
- `src/lib/reminders.ts` - File-based storage CRUD (`data/reminders.json`)
36+
37+
**API Endpoints:**
38+
- `POST /api/check` - Run check, send emails for stale repos
39+
- `POST /api/check?dryRun=1` - Preview without sending
40+
- `GET/POST/DELETE /api/reminders` - CRUD operations
41+
- `POST /api/test-email` - Verify Resend configuration
42+
43+
**Storage:** Reminders persist to `data/reminders.json`. For serverless deployment, replace with a database.
44+
45+
## Path Alias
46+
47+
`@/*` maps to `./src/*`

README.md

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,49 @@
1-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1+
# GitHub Reminder
22

3-
## Getting Started
3+
Email nudges when a repository has not been pushed in a while.
44

5-
First, run the development server:
5+
## Setup
6+
7+
1. Install dependencies:
8+
```bash
9+
npm install
10+
```
11+
2. Create `.env.local` with these values:
12+
```bash
13+
RESEND_API_KEY=
14+
RESEND_FROM=
15+
GITHUB_TOKEN=
16+
```
17+
18+
Optional: set `GITHUB_TOKEN` to avoid rate limits or access private repos.
19+
`RESEND_FROM` must be a verified sender in Resend.
20+
21+
## Run
622

723
```bash
824
npm run dev
9-
# or
10-
yarn dev
11-
# or
12-
pnpm dev
13-
# or
14-
bun dev
1525
```
1626

17-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
27+
Open `http://localhost:3000`.
1828

19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
29+
## Usage
2030

21-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
31+
- Add a reminder in the UI (owner/repo, email, threshold days).
32+
- Click **Run check now** to send emails for stale repos.
33+
- Click **Dry run** to preview without sending.
34+
- Use **Send test email** to verify Resend.
2235

23-
## Learn More
36+
## Cron / Scheduler
2437

25-
To learn more about Next.js, take a look at the following resources:
38+
Hit the check endpoint on a schedule:
2639

27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29-
30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
40+
```bash
41+
curl -X POST https://your-app.example.com/api/check
42+
```
3143

32-
## Deploy on Vercel
44+
Vercel Cron or a system cron job works fine.
3345

34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
46+
## Storage
3547

36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
48+
Reminders are stored in `data/reminders.json` (local file). If you deploy to
49+
serverless, move this to a real database.

package-lock.json

Lines changed: 67 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"dependencies": {
1212
"next": "16.1.3",
1313
"react": "19.2.3",
14-
"react-dom": "19.2.3"
14+
"react-dom": "19.2.3",
15+
"resend": "^6.7.0"
1516
},
1617
"devDependencies": {
1718
"@tailwindcss/postcss": "^4",

src/app/api/check/route.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { runReminderCheck } from "@/lib/checker";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export const runtime = "nodejs";
5+
6+
async function handleCheck(request: NextRequest) {
7+
const dryRun = request.nextUrl.searchParams.get("dryRun") === "1";
8+
try {
9+
const summary = await runReminderCheck({ dryRun });
10+
return NextResponse.json(summary);
11+
} catch (error) {
12+
return NextResponse.json(
13+
{ error: error instanceof Error ? error.message : "Check failed." },
14+
{ status: 500 },
15+
);
16+
}
17+
}
18+
19+
export async function GET(request: NextRequest) {
20+
return handleCheck(request);
21+
}
22+
23+
export async function POST(request: NextRequest) {
24+
return handleCheck(request);
25+
}

src/app/api/reminders/route.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { normalizeRepo } from "@/lib/github";
2+
import { addReminder, deleteReminder, listReminders } from "@/lib/reminders";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
export const runtime = "nodejs";
6+
7+
function isValidEmail(value: string) {
8+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
9+
}
10+
11+
function isValidRepo(value: string) {
12+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value);
13+
}
14+
15+
function parseThreshold(value: unknown) {
16+
const parsed = Number(value);
17+
if (!Number.isFinite(parsed)) {
18+
return null;
19+
}
20+
const rounded = Math.round(parsed);
21+
if (rounded < 1 || rounded > 365) {
22+
return null;
23+
}
24+
return rounded;
25+
}
26+
27+
export async function GET() {
28+
const reminders = await listReminders();
29+
return NextResponse.json({ reminders });
30+
}
31+
32+
export async function POST(request: NextRequest) {
33+
const payload = (await request.json()) as {
34+
email?: string;
35+
repo?: string;
36+
thresholdDays?: number;
37+
};
38+
39+
const email = payload.email?.trim() ?? "";
40+
const repo = normalizeRepo(payload.repo ?? "");
41+
const thresholdDays = parseThreshold(payload.thresholdDays ?? 3);
42+
43+
if (!isValidEmail(email)) {
44+
return NextResponse.json(
45+
{ error: "Invalid email address." },
46+
{ status: 400 },
47+
);
48+
}
49+
50+
if (!isValidRepo(repo)) {
51+
return NextResponse.json(
52+
{ error: "Repo must be in owner/name format." },
53+
{ status: 400 },
54+
);
55+
}
56+
57+
if (!thresholdDays) {
58+
return NextResponse.json(
59+
{ error: "Threshold must be between 1 and 365." },
60+
{ status: 400 },
61+
);
62+
}
63+
64+
const reminder = await addReminder({
65+
email,
66+
repo,
67+
thresholdDays,
68+
});
69+
70+
return NextResponse.json({ reminder }, { status: 201 });
71+
}
72+
73+
export async function DELETE(request: NextRequest) {
74+
const id = request.nextUrl.searchParams.get("id") ?? "";
75+
if (!id) {
76+
return NextResponse.json({ error: "Missing reminder id." }, { status: 400 });
77+
}
78+
const removed = await deleteReminder(id);
79+
return NextResponse.json({ ok: removed });
80+
}

src/app/api/test-email/route.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { sendReminderEmail } from "@/lib/mailer";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export const runtime = "nodejs";
5+
6+
export async function POST(request: NextRequest) {
7+
const payload = (await request.json()) as { email?: string };
8+
const email = payload.email?.trim() ?? "";
9+
10+
if (!email) {
11+
return NextResponse.json({ error: "Missing email." }, { status: 400 });
12+
}
13+
14+
try {
15+
await sendReminderEmail({
16+
to: email,
17+
subject: "GitHub reminder: test email",
18+
text: "This is a test email from GitHub Reminder.",
19+
html: "<p>This is a test email from <strong>GitHub Reminder</strong>.</p>",
20+
});
21+
} catch (error) {
22+
return NextResponse.json(
23+
{ error: error instanceof Error ? error.message : "Email failed." },
24+
{ status: 500 },
25+
);
26+
}
27+
28+
return NextResponse.json({ ok: true });
29+
}

0 commit comments

Comments
 (0)