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
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PedigreeCanvas } from '../PedigreeCanvas/PedigreeCanvas';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
import { PropertyPanel } from '../PropertyPanel/PropertyPanel';
|
||||
import { RelationshipPanel } from '../RelationshipPanel/RelationshipPanel';
|
||||
import { FilePanel } from '../FilePanel/FilePanel';
|
||||
import { WelcomeModal } from '../WelcomeModal/WelcomeModal';
|
||||
import { usePedigreeStore, useTemporalStore } from '@/store/pedigreeStore';
|
||||
import styles from './App.module.css';
|
||||
|
||||
const WELCOME_DISMISSED_KEY = 'pedigree-draw-welcome-dismissed';
|
||||
|
||||
export function App() {
|
||||
const {
|
||||
clearSelection,
|
||||
@@ -23,6 +26,15 @@ export function App() {
|
||||
} = usePedigreeStore();
|
||||
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
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -85,6 +97,8 @@ export function App() {
|
||||
<span>Pedigree Draw - For genetic counselors and bioinformatics professionals</span>
|
||||
<span>NSGC Standard Symbols</span>
|
||||
</footer>
|
||||
|
||||
{showWelcome && <WelcomeModal onClose={handleCloseWelcome} />}
|
||||
</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