Files
flvx/docs/superpowers/plans/2026-04-04-announcement-popup-plan.md
T
sagit 9a85363e44 feat: Announcement Popup Notification (#411)
* docs: add announcement popup design spec

* docs: add announcement popup implementation plan

* feat(api): include update_time in announcement response

* feat(ui): add update_time to AnnouncementData interface

* feat(ui): create AnnouncementModal component

* feat(ui): manage announcement modal state in dashboard hook

* feat(ui): add announcement modal to dashboard layout
2026-04-04 13:00:11 +08:00

7.4 KiB

Announcement Popup Notification Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a popup modal for announcements that automatically shows to users when a new or updated announcement is published.

Architecture: We will modify the Go backend to return update_time along with the announcement data. In the Vite frontend, we will store the user's flvx_announcement_seen_time in localStorage. If the fetched update_time is greater than the stored timestamp, we trigger a NextUI Modal displaying the announcement content.

Tech Stack: Go, Vite, React, TailwindCSS, NextUI.


Task 1: Update API Response in Go Backend

Files:

  • Modify: go-backend/internal/http/handler/handler.go

  • Step 1: Write the minimal implementation

Modify the getAnnouncement function in go-backend/internal/http/handler/handler.go. Find the response map inside getAnnouncement and add the update_time key:

	if ann == nil {
		response.WriteJSON(w, response.OK(map[string]interface{}{
			"content":     "",
			"enabled":     0,
			"update_time": 0,
		}))
		return
	}

	updateTime := ann.CreatedTime
	if ann.UpdatedTime.Valid {
		updateTime = ann.UpdatedTime.Int64
	}

	response.WriteJSON(w, response.OK(map[string]interface{}{
		"content":     ann.Content,
		"enabled":     ann.Enabled,
		"update_time": updateTime,
	}))
  • Step 2: Commit
git add go-backend/internal/http/handler/handler.go
git commit -m "feat(api): include update_time in announcement response"

Task 2: Update Frontend API Interface

Files:

  • Modify: vite-frontend/src/api/index.ts

  • Step 1: Write the minimal implementation

Modify the AnnouncementData interface in vite-frontend/src/api/index.ts to include update_time.

export interface AnnouncementData {
  content: string;
  enabled: number;
  update_time?: number;
}
  • Step 2: Commit
git add vite-frontend/src/api/index.ts
git commit -m "feat(ui): add update_time to AnnouncementData interface"

Task 3: Create AnnouncementModal Component

Files:

  • Create: vite-frontend/src/pages/dashboard/components/announcement-modal.tsx

  • Step 1: Write the minimal implementation

Create vite-frontend/src/pages/dashboard/components/announcement-modal.tsx with the following content:

import type { AnnouncementData } from "@/api";
import { Button } from "@/shadcn-bridge/heroui/button";
import {
  Modal,
  ModalBody,
  ModalContent,
  ModalFooter,
  ModalHeader,
} from "@/shadcn-bridge/heroui/modal";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

interface AnnouncementModalProps {
  announcement: AnnouncementData;
  isOpen: boolean;
  onClose: () => void;
  onDontShowAgain: () => void;
}

export const AnnouncementModal = ({
  announcement,
  isOpen,
  onClose,
  onDontShowAgain,
}: AnnouncementModalProps) => {
  return (
    <Modal isOpen={isOpen} onOpenChange={(open) => !open && onClose()} size="2xl">
      <ModalContent>
        <ModalHeader className="flex flex-col gap-1">平台公告</ModalHeader>
        <ModalBody>
          <div className="prose prose-sm dark:prose-invert max-w-none max-h-[60vh] overflow-y-auto">
            <ReactMarkdown remarkPlugins={[remarkGfm]}>
              {announcement.content}
            </ReactMarkdown>
          </div>
        </ModalBody>
        <ModalFooter>
          <Button variant="flat" onPress={onDontShowAgain}>
            不再提示
          </Button>
          <Button color="primary" onPress={onClose}>
            关闭
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
};
  • Step 2: Commit
git add vite-frontend/src/pages/dashboard/components/announcement-modal.tsx
git commit -m "feat(ui): create AnnouncementModal component"

Task 4: Integrate Modal State in Dashboard Custom Hook

Files:

  • Modify: vite-frontend/src/pages/dashboard/use-dashboard-data.ts

  • Step 1: Update the hook return type interface

At the top of vite-frontend/src/pages/dashboard/use-dashboard-data.ts where DashboardData is or similar, add the new properties (if it uses an explicit return type). If it's inferred, skip this. Wait, let's check the code:

  isAnnouncementModalOpen: boolean;
  setIsAnnouncementModalOpen: (isOpen: boolean) => void;
  dismissAnnouncementModal: () => void;

Ensure they are added to the returned object at the bottom of the useDashboardData hook.

Find the const loadAnnouncement function.

  • Step 2: Write the minimal implementation

First, add state at the top of the hook:

  const [isAnnouncementModalOpen, setIsAnnouncementModalOpen] = useState(false);

Then, modify the loadAnnouncement logic inside useDashboardData:

      if (res.code === 0 && res.data && res.data.enabled === 1) {
        setAnnouncement(res.data);
        
        try {
          const storedTimeStr = localStorage.getItem("flvx_announcement_seen_time");
          const storedTime = storedTimeStr ? parseInt(storedTimeStr, 10) : 0;
          const updateTime = res.data.update_time || 0;
          
          if (updateTime > storedTime) {
            setIsAnnouncementModalOpen(true);
          }
        } catch (err) {
          console.warn("Failed to read localStorage for announcement state", err);
          setIsAnnouncementModalOpen(true);
        }
      } else {
        setAnnouncement(null);
      }

Add the dismiss handler inside the hook:

  const dismissAnnouncementModal = useCallback(() => {
    setIsAnnouncementModalOpen(false);
    if (announcement && announcement.update_time) {
      try {
        localStorage.setItem("flvx_announcement_seen_time", announcement.update_time.toString());
      } catch (err) {
        console.warn("Failed to set localStorage for announcement state", err);
      }
    }
  }, [announcement]);

Ensure these are included in the return object of the hook:

    isAnnouncementModalOpen,
    setIsAnnouncementModalOpen,
    dismissAnnouncementModal,
  • Step 3: Commit
git add vite-frontend/src/pages/dashboard/use-dashboard-data.ts
git commit -m "feat(ui): manage announcement modal state in dashboard hook"

Task 5: Add Modal to Dashboard Layout

Files:

  • Modify: vite-frontend/src/pages/dashboard.tsx

  • Step 1: Write the minimal implementation

Import the modal component at the top:

import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";

Add the new properties to the destructured useDashboardData object:

    isAnnouncementModalOpen,
    setIsAnnouncementModalOpen,
    dismissAnnouncementModal,

Add the modal instance near the end of the dashboard rendering (just below {announcement && <AnnouncementBanner ... />} or inside the main <div>):

      {announcement && (
        <AnnouncementModal
          announcement={announcement}
          isOpen={isAnnouncementModalOpen}
          onClose={() => setIsAnnouncementModalOpen(false)}
          onDontShowAgain={dismissAnnouncementModal}
        />
      )}
  • Step 2: Commit
git add vite-frontend/src/pages/dashboard.tsx
git commit -m "feat(ui): add announcement modal to dashboard layout"