From e1ff8a5f112bb2b0b951f2fb836c1ecd1211529f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 10:08:52 +1000 Subject: [PATCH 1/6] feat: Handle inactive users --- .../billing/jobs/cleanup-inactive-pages.ts | 93 +++++++++ .../migrations/17_handle_inactive_pages.sql | 45 +++++ packages/supabase/types/index.ts | 177 ++++++++++-------- 3 files changed, 240 insertions(+), 75 deletions(-) create mode 100644 apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts create mode 100644 packages/supabase/migrations/17_handle_inactive_pages.sql diff --git a/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts b/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts new file mode 100644 index 0000000..e57ad1e --- /dev/null +++ b/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts @@ -0,0 +1,93 @@ +import { supabaseAdmin } from "@changes-page/supabase/admin"; +import { IErrorResponse } from "@changes-page/supabase/types/api"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { v4 } from "uuid"; + +interface CleanupResponse { + status: string; + deletedPages: number; + jobId: string; +} + +const cleanupInactivePagesJob = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + if (req.method !== "POST") { + return res + .status(405) + .json({ error: { statusCode: 405, message: "Method not allowed" } }); + } + + try { + const jobId = v4(); + + console.log(`[${jobId}] Starting cleanup inactive pages job`); + + const { data: inactivePages, error: fetchError } = await supabaseAdmin.rpc( + "get_pages_with_inactive_subscriptions" + ); + + if (fetchError) { + console.error(`[${jobId}] Error fetching inactive pages:`, fetchError); + throw fetchError; + } + + const allInactivePages = inactivePages || []; + + console.log( + `[${jobId}] Found ${allInactivePages.length} pages from inactive users to delete` + ); + + if (allInactivePages.length === 0) { + console.log(`[${jobId}] No pages to delete`); + return res.status(200).json({ + status: "ok", + deletedPages: 0, + jobId, + }); + } + + const pageIdsToDelete = allInactivePages.map((page) => page.page_id); + + console.log(`[${jobId}] Deleting ${pageIdsToDelete.length} pages`); + const { error: pagesDeleteError } = await supabaseAdmin + .from("pages") + .delete() + .in("id", pageIdsToDelete); + + if (pagesDeleteError) { + console.error(`[${jobId}] Error deleting pages:`, pagesDeleteError); + throw pagesDeleteError; + } + + console.log( + `[${jobId}] Successfully deleted ${pageIdsToDelete.length} pages and related data` + ); + + // Log deleted pages for audit purposes + allInactivePages.forEach((page) => { + console.log( + `[${jobId}] Deleted page: ${page.page_title} (ID: ${page.page_id}) from user: ${page.user_id}` + ); + }); + + console.log(`[${jobId}] Cleanup job finished successfully`); + + return res.status(200).json({ + status: "ok", + deletedPages: pageIdsToDelete.length, + jobId, + }); + } catch (err) { + console.error("cleanupInactivePagesJob error:", err); + res.status(500).json({ + error: { + statusCode: 500, + message: err.message || "Internal server error", + }, + }); + } +}; + +export default cleanupInactivePagesJob; diff --git a/packages/supabase/migrations/17_handle_inactive_pages.sql b/packages/supabase/migrations/17_handle_inactive_pages.sql new file mode 100644 index 0000000..f5c46b1 --- /dev/null +++ b/packages/supabase/migrations/17_handle_inactive_pages.sql @@ -0,0 +1,45 @@ +-- Drop the existing foreign key constraint +ALTER TABLE page_audit_logs +DROP CONSTRAINT page_audit_logs_page_id_fkey; + +-- Recreate the constraint with CASCADE DELETE +ALTER TABLE page_audit_logs +ADD CONSTRAINT page_audit_logs_page_id_fkey +FOREIGN KEY (page_id) +REFERENCES pages(id) +ON DELETE CASCADE; + +-- Function to get pages with inactive subscriptions +CREATE OR REPLACE FUNCTION get_pages_with_inactive_subscriptions() +RETURNS TABLE ( + page_id uuid, + page_title text, + page_created_at timestamptz, + url text, + user_id uuid +) AS $$ +BEGIN + RETURN QUERY + SELECT + p.id AS page_id, + p.title AS page_title, + p.created_at AS page_created_at, + p.url_slug AS url, + u.id AS user_id + FROM + pages p + JOIN + users u ON p.user_id = u.id + WHERE + ( + -- Users with canceled subscription + (u.stripe_subscription->>'status')::text = 'canceled' + -- OR users without any subscription + OR u.stripe_subscription IS NULL + ) + AND u.pro_gifted = false + AND p.created_at < NOW() - INTERVAL '90 days' + ORDER BY + p.created_at ASC; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; \ No newline at end of file diff --git a/packages/supabase/types/index.ts b/packages/supabase/types/index.ts index 6b653b2..4034a99 100644 --- a/packages/supabase/types/index.ts +++ b/packages/supabase/types/index.ts @@ -7,6 +7,11 @@ export type Json = | Json[] export type Database = { + // Allows to automatically instanciate createClient with right options + // instead of createClient(URL, KEY) + __InternalSupabase: { + PostgrestVersion: "10.2.0 (e07807d)" + } public: { Tables: { page_audit_logs: { @@ -553,60 +558,51 @@ export type Database = { [_ in never]: never } Functions: { - is_subscription_active: { - Args: { + get_pages_with_inactive_subscriptions: { + Args: Record + Returns: { + page_id: string + page_title: string + page_created_at: string + url: string user_id: string - } + }[] + } + is_subscription_active: { + Args: { user_id: string } Returns: boolean } is_team_member: { - Args: { - tid: string - uid: string - } + Args: { tid: string; uid: string } Returns: boolean } page_view_browsers: { - Args: { - pageid: string - date: string - } + Args: { pageid: string; date: string } Returns: { data_name: string data_count: number }[] } page_view_os: { - Args: { - pageid: string - date: string - } + Args: { pageid: string; date: string } Returns: { data_name: string data_count: number }[] } page_view_referrers: { - Args: { - pageid: string - date: string - } + Args: { pageid: string; date: string } Returns: { data_name: string data_count: number }[] } page_view_stats: { - Args: { - pageid: string - date: string - } + Args: { pageid: string; date: string } Returns: Record } post_reactions_aggregate: { - Args: { - postid: string - } + Args: { postid: string } Returns: { thumbs_up_count: number thumbs_down_count: number @@ -628,27 +624,33 @@ export type Database = { } } -type PublicSchema = Database[Extract] +type DatabaseWithoutInternals = Omit + +type DefaultSchema = DatabaseWithoutInternals[Extract] export type Tables< - PublicTableNameOrOptions extends - | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & - Database[PublicTableNameOrOptions["schema"]]["Views"]) + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & - Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { Row: infer R } ? R : never - : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & - PublicSchema["Views"]) - ? (PublicSchema["Tables"] & - PublicSchema["Views"])[PublicTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & + DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & + DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { Row: infer R } ? R @@ -656,20 +658,24 @@ export type Tables< : never export type TablesInsert< - PublicTableNameOrOptions extends - | keyof PublicSchema["Tables"] - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { Insert: infer I } ? I : never - : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] - ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { Insert: infer I } ? I @@ -677,20 +683,24 @@ export type TablesInsert< : never export type TablesUpdate< - PublicTableNameOrOptions extends - | keyof PublicSchema["Tables"] - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { Update: infer U } ? U : never - : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] - ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { Update: infer U } ? U @@ -698,29 +708,46 @@ export type TablesUpdate< : never export type Enums< - PublicEnumNameOrOptions extends - | keyof PublicSchema["Enums"] - | { schema: keyof Database }, - EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema["Enums"] + | { schema: keyof DatabaseWithoutInternals }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] : never = never, -> = PublicEnumNameOrOptions extends { schema: keyof Database } - ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] - : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] - ? PublicSchema["Enums"][PublicEnumNameOrOptions] +> = DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] + ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] : never export type CompositeTypes< PublicCompositeTypeNameOrOptions extends - | keyof PublicSchema["CompositeTypes"] - | { schema: keyof Database }, + | keyof DefaultSchema["CompositeTypes"] + | { schema: keyof DatabaseWithoutInternals }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database + schema: keyof DatabaseWithoutInternals } - ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] : never = never, -> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } - ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] - : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] - ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] +> = PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] + ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] : never + +export const Constants = { + public: { + Enums: { + page_color_scheme: ["auto", "dark", "light"], + page_type: ["changelogs", "updates", "releases", "announcements"], + post_status: ["draft", "published", "archived", "publish_later"], + post_type: ["fix", "new", "improvement", "announcement", "alert"], + }, + }, +} as const From a86b2cc15bcc10589d79e919b121ae10c81a4baf Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 11:03:35 +1000 Subject: [PATCH 2/6] Handle custom domain removal --- apps/web/pages/api/pages/settings/webhook.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/web/pages/api/pages/settings/webhook.ts b/apps/web/pages/api/pages/settings/webhook.ts index 9661552..c69a57b 100644 --- a/apps/web/pages/api/pages/settings/webhook.ts +++ b/apps/web/pages/api/pages/settings/webhook.ts @@ -21,6 +21,25 @@ const databaseWebhook = async (req: NextApiRequest, res: NextApiResponse) => { page_id ); + if (type === "DELETE") { + if (page_settings?.custom_domain) { + try { + const response = await fetch( + `https://api.vercel.com/v8/projects/${process.env.VERCEL_PAGES_PROJECT_ID}/domains/${page_settings.custom_domain}?teamId=${process.env.VERCEL_TEAM_ID}`, + { + headers: { + Authorization: `Bearer ${process.env.VERCEL_AUTH_TOKEN}`, + }, + method: "DELETE", + } + ).then((res) => res.json()); + console.log("Response from Vercel API:", response); + } catch (error) { + console.error("Error deleting custom domain:", error); + } + } + } + return res.status(200).json({ ok: true }); } catch (err) { console.log("Trigger databaseWebhook [Page Settings]: Error:", err); From 926c48741ea6c1ba1e66b2da06dcef61b42b511e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 14:34:46 +1000 Subject: [PATCH 3/6] Update inactive user query --- packages/supabase/migrations/17_handle_inactive_pages.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/supabase/migrations/17_handle_inactive_pages.sql b/packages/supabase/migrations/17_handle_inactive_pages.sql index f5c46b1..bea2083 100644 --- a/packages/supabase/migrations/17_handle_inactive_pages.sql +++ b/packages/supabase/migrations/17_handle_inactive_pages.sql @@ -30,6 +30,8 @@ BEGIN pages p JOIN users u ON p.user_id = u.id + JOIN + auth.users au ON u.id = au.id WHERE ( -- Users with canceled subscription @@ -37,8 +39,10 @@ BEGIN -- OR users without any subscription OR u.stripe_subscription IS NULL ) + -- not gifted pro AND u.pro_gifted = false - AND p.created_at < NOW() - INTERVAL '90 days' + -- User hasn't been active in the last 180 days + AND (au.last_sign_in_at IS NULL OR au.last_sign_in_at < NOW() - INTERVAL '180 days') ORDER BY p.created_at ASC; END; From 213e13d45ff9c91c5efdec640c0276f0c4653e73 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 14:54:51 +1000 Subject: [PATCH 4/6] Add cron auth --- .../pages/api/billing/jobs/cleanup-inactive-pages.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts b/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts index e57ad1e..6a46de7 100644 --- a/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts +++ b/apps/web/pages/api/billing/jobs/cleanup-inactive-pages.ts @@ -19,6 +19,16 @@ const cleanupInactivePagesJob = async ( .json({ error: { statusCode: 405, message: "Method not allowed" } }); } + const authHeader = req.headers["authorization"]; + if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { + return res.status(401).json({ + error: { + statusCode: 401, + message: "Unauthorized", + }, + }); + } + try { const jobId = v4(); From 068159d37e9ce65212afa1ff648f0c6944a62ed2 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 14:59:30 +1000 Subject: [PATCH 5/6] Setup cronjob --- apps/web/vercel.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/vercel.json b/apps/web/vercel.json index 9353095..5f34883 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -3,6 +3,10 @@ { "path": "/api/billing/jobs/report-usage", "schedule": "0 0 * * *" + }, + { + "path": "/api/billing/jobs/cleanup-inactive-pages", + "schedule": "0 1 * * *" } ] -} +} \ No newline at end of file From 8871ea656286a2735cb4756a3f2ed65756039c8f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 27 Jul 2025 18:38:54 +1000 Subject: [PATCH 6/6] Add docker compose files --- Dockerfile.page | 51 +++++++++++++++++++++++++++++++++++ Dockerfile.web | 51 +++++++++++++++++++++++++++++++++++ apps/page/components/post.tsx | 5 +++- apps/page/package.json | 2 +- docker-compose.yml | 23 ++++++++++++++++ 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.page create mode 100644 Dockerfile.web create mode 100644 docker-compose.yml diff --git a/Dockerfile.page b/Dockerfile.page new file mode 100644 index 0000000..80163cb --- /dev/null +++ b/Dockerfile.page @@ -0,0 +1,51 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml ./ +COPY apps/page/package.json ./apps/page/ +COPY packages/ ./packages/ +RUN corepack enable pnpm && pnpm i --frozen-lockfile + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the page application +RUN corepack enable pnpm && cd apps/page && pnpm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/apps/page/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/apps/page/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/page/.next/static ./.next/static + +USER nextjs + +EXPOSE 3001 + +ENV PORT 3001 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "apps/page/server.js"] \ No newline at end of file diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..c86bf14 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,51 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml ./ +COPY apps/web/package.json ./apps/web/ +COPY packages/ ./packages/ +RUN corepack enable pnpm && pnpm i --frozen-lockfile + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the web application +RUN corepack enable pnpm && cd apps/web && pnpm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/apps/web/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "apps/web/server.js"] \ No newline at end of file diff --git a/apps/page/components/post.tsx b/apps/page/components/post.tsx index 07851ea..0a55bda 100644 --- a/apps/page/components/post.tsx +++ b/apps/page/components/post.tsx @@ -18,7 +18,10 @@ const PostDateTime = dynamic( { ssr: false, } -); +) as React.ComponentType<{ + publishedAt: string; + startWithFullDate?: boolean; +}>; export default function Post({ post, diff --git a/apps/page/package.json b/apps/page/package.json index b6c7c37..81c4021 100644 --- a/apps/page/package.json +++ b/apps/page/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev --port 3001", - "build": "mkdir public/v1 && minify widget/widget.js > public/v1/widget.js && next build", + "build": "mkdir -p public/v1 && minify widget/widget.js > public/v1/widget.js && next build", "start": "next start", "lint": "next lint" }, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..84482b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + web: + build: + context: . + dockerfile: Dockerfile.web + ports: + - "3000:3000" + env_file: + - apps/web/.env + environment: + - NODE_ENV=production + + page: + build: + context: . + dockerfile: Dockerfile.page + ports: + - "3001:3001" + env_file: + - apps/page/.env + environment: + - NODE_ENV=production +