Add welcome modal with author info and privacy policy
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:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/components/WelcomeModal/WelcomeModal.module.css
Normal file
92
src/components/WelcomeModal/WelcomeModal.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/components/WelcomeModal/WelcomeModal.tsx
Normal file
75
src/components/WelcomeModal/WelcomeModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user