Add welcome modal with author info and privacy policy
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

Features:
- Display welcome popup on first visit
- Auto-detect browser language (Chinese/English)
- Show author motivation (Usher syndrome research support)
- Privacy policy: no data collection, browser-only operations
- Use localStorage to remember dismissal
- Responsive design for mobile devices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gbanyan
2025-12-16 01:20:24 +08:00
parent b72b8e2811
commit 7c2a624130
3 changed files with 182 additions and 1 deletions

View File

@@ -4,15 +4,18 @@
* Main application layout * Main application layout
*/ */
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { PedigreeCanvas } from '../PedigreeCanvas/PedigreeCanvas'; import { PedigreeCanvas } from '../PedigreeCanvas/PedigreeCanvas';
import { Toolbar } from '../Toolbar/Toolbar'; import { Toolbar } from '../Toolbar/Toolbar';
import { PropertyPanel } from '../PropertyPanel/PropertyPanel'; import { PropertyPanel } from '../PropertyPanel/PropertyPanel';
import { RelationshipPanel } from '../RelationshipPanel/RelationshipPanel'; import { RelationshipPanel } from '../RelationshipPanel/RelationshipPanel';
import { FilePanel } from '../FilePanel/FilePanel'; import { FilePanel } from '../FilePanel/FilePanel';
import { WelcomeModal } from '../WelcomeModal/WelcomeModal';
import { usePedigreeStore, useTemporalStore } from '@/store/pedigreeStore'; import { usePedigreeStore, useTemporalStore } from '@/store/pedigreeStore';
import styles from './App.module.css'; import styles from './App.module.css';
const WELCOME_DISMISSED_KEY = 'pedigree-draw-welcome-dismissed';
export function App() { export function App() {
const { const {
clearSelection, clearSelection,
@@ -23,6 +26,15 @@ export function App() {
} = usePedigreeStore(); } = usePedigreeStore();
const temporal = useTemporalStore(); const temporal = useTemporalStore();
// Welcome modal state - check localStorage on init
const [showWelcome, setShowWelcome] = useState(() => {
return localStorage.getItem(WELCOME_DISMISSED_KEY) !== 'true';
});
const handleCloseWelcome = () => {
setShowWelcome(false);
};
// Keyboard shortcuts // Keyboard shortcuts
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -85,6 +97,8 @@ export function App() {
<span>Pedigree Draw - For genetic counselors and bioinformatics professionals</span> <span>Pedigree Draw - For genetic counselors and bioinformatics professionals</span>
<span>NSGC Standard Symbols</span> <span>NSGC Standard Symbols</span>
</footer> </footer>
{showWelcome && <WelcomeModal onClose={handleCloseWelcome} />}
</div> </div>
); );
} }

View File

@@ -0,0 +1,92 @@
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 520px;
width: 100%;
padding: 32px;
max-height: 90vh;
overflow-y: auto;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1976D2;
text-align: center;
margin: 0 0 24px 0;
}
.section {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 14px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.paragraph {
font-size: 14px;
line-height: 1.6;
color: #333;
margin: 0 0 12px 0;
}
.paragraph:last-child {
margin-bottom: 0;
}
.button {
display: block;
width: 100%;
padding: 14px 24px;
background: #1976D2;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
.button:hover {
background: #1565C0;
}
.button:active {
background: #0D47A1;
}
/* Responsive design */
@media (max-width: 600px) {
.modal {
padding: 24px;
}
.title {
font-size: 20px;
}
.paragraph {
font-size: 13px;
}
}

View File

@@ -0,0 +1,75 @@
/**
* WelcomeModal Component
*
* Displays author information and privacy policy on first visit.
* Uses localStorage to remember dismissal.
* Automatically detects browser language for Chinese/English content.
*/
import styles from './WelcomeModal.module.css';
interface WelcomeModalProps {
onClose: () => void;
}
const isChineseLocale = (): boolean => {
const lang = navigator.language || (navigator.languages?.[0]) || 'en';
return lang.toLowerCase().startsWith('zh');
};
export function WelcomeModal({ onClose }: WelcomeModalProps) {
const isChinese = isChineseLocale();
const content = isChinese ? {
title: '歡迎使用 Pedigree Draw',
aboutTitle: '關於本專案',
aboutContent: [
'本專案由一位患有 Usher syndrome 的作者所建立。',
'建立此工具的動機是為了支援遺傳學、系譜學和 Bioinformatics 領域的研究,並探索視覺化工具如何幫助理解遺傳性疾病。',
'作者明確支持與 Usher syndrome 及其他遺傳疾病相關的研究,並希望此專案能以某種方式為 Bioinformatics 研究社群做出貢獻。',
],
privacyTitle: '隱私政策',
privacyContent: '本網站不搜集任何使用者資料,也不儲存任何資料。所有操作僅在您的瀏覽器中進行,關閉頁面後資料即消失。',
buttonText: '我了解,開始使用',
} : {
title: 'Welcome to Pedigree Draw',
aboutTitle: 'About This Project',
aboutContent: [
'This project is created by an author with Usher syndrome.',
'The motivation behind this work is to support research in genetics, genealogy, and bioinformatics, and to explore how visualization tools may assist in understanding hereditary conditions.',
'The author explicitly supports research related to Usher syndrome and other genetic disorders, and hopes this project may contribute, even in a small way, to the bioinformatics research community.',
],
privacyTitle: 'Privacy Policy',
privacyContent: 'This website does not collect any user data or store any information. All operations are performed locally in your browser. Data will be lost when you close the page.',
buttonText: 'I Understand, Let\'s Start',
};
const handleClose = () => {
localStorage.setItem('pedigree-draw-welcome-dismissed', 'true');
onClose();
};
return (
<div className={styles.overlay} onClick={handleClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<h1 className={styles.title}>{content.title}</h1>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{content.aboutTitle}</h2>
{content.aboutContent.map((paragraph, index) => (
<p key={index} className={styles.paragraph}>{paragraph}</p>
))}
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{content.privacyTitle}</h2>
<p className={styles.paragraph}>{content.privacyContent}</p>
</section>
<button className={styles.button} onClick={handleClose}>
{content.buttonText}
</button>
</div>
</div>
);
}