diff --git a/apps/web/components/Footer.tsx b/apps/web/components/Footer.tsx index 1e283a9..0602dcb 100644 --- a/apps/web/components/Footer.tsx +++ b/apps/web/components/Footer.tsx @@ -31,6 +31,7 @@ export default function Footer() { Browse Home All Stores + Forums Search Submit a Code Bounties diff --git a/apps/web/components/Header.tsx b/apps/web/components/Header.tsx index 559deee..a50ec5e 100644 --- a/apps/web/components/Header.tsx +++ b/apps/web/components/Header.tsx @@ -26,6 +26,10 @@ export default function Header() { Stores + {/* A plain : /bbs is the tsbb board nginx serves, not a Next route. */} + + Forums + Bounties @@ -75,6 +79,9 @@ export default function Header() { setMenuOpen(false)}> Stores + + Forums + setMenuOpen(false)}> Bounties diff --git a/ops/bbs/README.md b/ops/bbs/README.md new file mode 100644 index 0000000..09673a9 --- /dev/null +++ b/ops/bbs/README.md @@ -0,0 +1,23 @@ +# c0upons.com/bbs + +The forums at https://c0upons.com/bbs are a [tsbb](https://github.com/profullstack/tsbb) +board, not part of this app. nginx sends `/bbs` and `/bbs/*` to it before the Next.js +app ever sees the request (cli-tools `dev2/sites.d/c0upons.com.json`, `nginx_locations`). + +| | | +| --- | --- | +| Box | dev2, `/home/anthony/www/c0upons.com-bbs`, container `c0uponscom-bbs-app-1`, `127.0.0.1:3293` | +| Base URL | `TSBB_BASE_URL=https://c0upons.com/bbs` (tsbb >= 0.9.0 mounts itself under the path) | +| Data | SQLite on the volume: `volumes/data/tsbb.db` | +| Secrets | logicsrc vault `c0upons-bbs--prod` | +| Updates | follows tsbb GitHub releases on its own (`TSBB_CHECKOUT_DIR`), every 5 minutes | +| Admin | `anthony` (anthony@profullstack.com), sign in at /bbs/login | + +`seed-forums.ts` creates the Slickdeals-style forum tree and the c0upons branding. +It is idempotent: forums are matched by slug, so re-running only adds what is missing. + +```sh +docker cp ops/bbs/seed-forums.ts c0uponscom-bbs-app-1:/app/data/seed-forums.ts +docker exec c0uponscom-bbs-app-1 node /app/data/seed-forums.ts +docker restart c0uponscom-bbs-app-1 # settings are cached per process +``` diff --git a/ops/bbs/seed-forums.ts b/ops/bbs/seed-forums.ts new file mode 100644 index 0000000..55812df --- /dev/null +++ b/ops/bbs/seed-forums.ts @@ -0,0 +1,104 @@ +/** + * The c0upons.com/bbs forum tree and branding: a Slickdeals-style deal board. + * + * Runs inside the tsbb container, against the board's own database: + * + * docker cp ops/bbs/seed-forums.ts c0uponscom-bbs-app-1:/app/data/seed-forums.ts + * docker exec c0uponscom-bbs-app-1 node /app/data/seed-forums.ts + * docker restart c0uponscom-bbs-app-1 # settings are cached per process + * + * Idempotent. Forums are matched by slug, so re-running adds what is missing + * and leaves admin edits (names, order) alone. It runs before tsbb's own seed, + * which then skips its generic starter tree because forums already exist, and + * fills in groups, permissions and ranks as usual. + */ +const CHECKOUT = process.env.TSBB_CHECKOUT_DIR ?? '/app'; +const { migrate } = await import(`${CHECKOUT}/packages/db/src/migrate.ts`); +const { one, run, now } = await import(`${CHECKOUT}/packages/db/src/client.ts`); +const { seed } = await import(`${CHECKOUT}/packages/db/src/seed.ts`); + +type Forum = { slug: string; name: string; description: string }; +type Category = { slug: string; name: string; forums: Forum[] }; + +const TREE: Category[] = [ + { + slug: 'deals', + name: 'Deals', + forums: [ + { slug: 'hot-deals', name: 'Hot Deals', description: 'Found a price worth sharing? Post the store, the price and the link. The best ones get voted up.' }, + { slug: 'coupons', name: 'Coupons & Promo Codes', description: 'Working codes, printable coupons, cashback and stacking tricks. Say where and when it worked.' }, + { slug: 'freebies', name: 'Freebies', description: 'Free samples, free-after-rebate, free trials actually worth having.' }, + { slug: 'grocery', name: 'Grocery & Weekly Ads', description: 'Weekly flyer highlights, digital coupons and grocery stacks.' }, + { slug: 'tech-deals', name: 'Tech Deals', description: 'Laptops, phones, games, gadgets and software.' }, + { slug: 'home-deals', name: 'Home & Garden', description: 'Appliances, tools, furniture and everything for the house.' }, + { slug: 'fashion-deals', name: 'Clothing & Beauty', description: 'Apparel, shoes, accessories and beauty sales.' }, + { slug: 'travel-deals', name: 'Travel Deals', description: 'Flights, hotels, error fares and points tricks.' }, + ], + }, + { + slug: 'community', + name: 'Community', + forums: [ + { slug: 'deal-requests', name: 'Deal Requests', description: 'Looking for something? Ask, and someone will hunt down the best price.' }, + { slug: 'deal-talk', name: 'Deal Talk', description: 'Price history, store policies, price matching, return wins and losses.' }, + { slug: 'contests', name: 'Contests & Giveaways', description: 'Sweepstakes and giveaways worth entering.' }, + { slug: 'off-topic', name: 'Off Topic', description: 'Anything that is not a deal.' }, + ], + }, + { + slug: 'c0upons', + name: 'c0upons', + forums: [ + { slug: 'announcements', name: 'Announcements', description: 'News about c0upons.com and these forums.' }, + { slug: 'feedback', name: 'Feedback & Bugs', description: 'Something broken, or an idea for the site? Tell us here.' }, + ], + }, +]; + +const SETTINGS: Record = { + 'board.name': 'c0upons forums', + 'board.tagline': 'Hot deals, coupon codes and freebies, shared by the people who found them.', + 'board.description': + 'The c0upons.com community: post hot deals, working promo codes and freebies, ask for a deal, and vote up the best finds.', + 'board.skin': 'classic', + 'board.accent': '#f97316', + 'board.logoUrl': 'https://c0upons.com/logo.svg', + 'board.logoHref': 'https://c0upons.com', + 'board.faviconUrl': 'https://c0upons.com/favicon.svg', + // The tsbb platform pitch on the index is for boards that are about tsbb. + 'board.showPlatform': false, +}; + +await migrate(undefined, { quiet: true }); + +async function upsertForum(parentId: number | null, kind: string, slug: string, name: string, description: string | null, position: number) { + const existing = await one<{ id: number }>('SELECT id FROM forums WHERE slug = ?', [slug]); + if (existing) return Number(existing.id); + const result = await run( + `INSERT INTO forums (parent_id, kind, slug, name, description, position, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id`, + [parentId, kind, slug, name, description, position, now()], + ); + console.log(` + ${kind} ${slug}`); + return Number((result.rows[0] as unknown as { id: number }).id); +} + +for (const [c, category] of TREE.entries()) { + const parent = await upsertForum(null, 'category', category.slug, category.name, null, c); + for (const [f, forum] of category.forums.entries()) { + await upsertForum(parent, 'forum', forum.slug, forum.name, forum.description, f); + } +} + +for (const [key, value] of Object.entries(SETTINGS)) { + await run( + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) + ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`, + [key, JSON.stringify(value), now()], + ); +} +console.log(' settings'); + +await seed({ quiet: true }); +console.log('Done. Restart the container so the running board reloads its settings.'); +process.exit(0);