Skip to content

Commit 0f11596

Browse files
authored
Show popup welcome screen (#171)
1 parent 823485b commit 0f11596

File tree

6 files changed

+201
-4
lines changed

6 files changed

+201
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![npm](https://img.shields.io/npm/v/hyperparam)](https://www.npmjs.com/package/hyperparam)
44
[![workflow status](https://github.com/hyparam/hyperparam-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/hyperparam-cli/actions)
55
[![mit license](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6-
![coverage](https://img.shields.io/badge/Coverage-35-darkred)
6+
![coverage](https://img.shields.io/badge/Coverage-51-darkred)
77

88
This is the hyperparam cli tool.
99

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"build": "run-s build:lib build:types build:app",
3434
"coverage": "vitest run -c vite.lib.config.js --coverage --coverage.include=src --coverage.include=bin",
3535
"dev": "run-p -l watch:ts watch:vite watch:serve",
36-
"lint": "eslint .",
36+
"lint": "eslint",
37+
"lint:fix": "eslint --fix",
3738
"prepublishOnly": "npm run build",
3839
"serve": "node bin/cli.js",
3940
"preserve": "npm run build",
@@ -47,7 +48,7 @@
4748
},
4849
"dependencies": {
4950
"hightable": "0.12.1",
50-
"hyparquet": "1.8.6",
51+
"hyparquet": "1.8.7",
5152
"hyparquet-compressors": "1.0.0",
5253
"react": "18.3.1",
5354
"react-dom": "18.3.1"

src/components/Layout.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReactNode, useEffect, useState } from 'react'
22
import { cn } from '../lib/utils.js'
3+
import Welcome from './Welcome.js'
34

45
interface LayoutProps {
56
children: ReactNode
@@ -21,10 +22,25 @@ interface LayoutProps {
2122
* @param props.title - page title
2223
*/
2324
export default function Layout({ children, className, progress, error, title }: LayoutProps) {
25+
const [showWelcome, setShowWelcome] = useState(false)
26+
27+
// Check localStorage on mount to see if the user has seen the welcome popup
28+
useEffect(() => {
29+
const dismissed = localStorage.getItem('welcome:dismissed') === 'true'
30+
setShowWelcome(!dismissed)
31+
}, [])
32+
33+
// Handle closing the welcome popup
34+
function handleCloseWelcome() {
35+
setShowWelcome(false)
36+
localStorage.setItem('welcome:dismissed', 'true')
37+
}
38+
2439
// Update title
2540
useEffect(() => {
2641
document.title = title ? `${title} - hyperparam` : 'hyperparam'
2742
}, [title])
43+
2844
return <main className='main'>
2945
<Sidebar />
3046
<div className='content-container'>
@@ -38,6 +54,7 @@ export default function Layout({ children, className, progress, error, title }:
3854
<div style={{ width: `${100 * progress}%` }} />
3955
</div>
4056
}
57+
{showWelcome && <Welcome onClose={handleCloseWelcome} />}
4158
</main>
4259
}
4360

src/components/Welcome.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { MouseEvent, useEffect } from 'react'
2+
3+
interface WelcomePopupProps {
4+
onClose: () => void
5+
}
6+
7+
/**
8+
* Welcome popup component shown to first-time users.
9+
* Clicking outside the popup or pressing Escape will dismiss it.
10+
*/
11+
export default function Welcome({ onClose }: WelcomePopupProps) {
12+
// Close popup when clicking outside
13+
function handleBackdropClick(e: MouseEvent) {
14+
if (e.target === e.currentTarget) {
15+
onClose()
16+
}
17+
}
18+
19+
// Close popup when pressing Escape key
20+
useEffect(() => {
21+
function handleEscKey(e: KeyboardEvent) {
22+
if (e.key === 'Escape') {
23+
onClose()
24+
}
25+
}
26+
27+
window.addEventListener('keydown', handleEscKey)
28+
return () => { window.removeEventListener('keydown', handleEscKey) }
29+
}, [onClose])
30+
31+
return (
32+
<div className="welcome" onClick={handleBackdropClick}>
33+
<div>
34+
<h2>npx hyperparam</h2>
35+
<p>
36+
This is the <a href="https://hyperparam.app">Hyperparam</a> cli for local data viewing.
37+
</p>
38+
<p>
39+
This tool lets you browse and explore large datasets particularly in parquet format.
40+
</p>
41+
<p>
42+
Supported file types include Parquet, CSV, JSON, Markdown, and Text.
43+
</p>
44+
<button onClick={onClose}>
45+
Got it
46+
</button>
47+
</div>
48+
</div>
49+
)
50+
}

src/styles/app.css

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@
55
padding: 0;
66
}
77

8+
button {
9+
background-color: #111;
10+
color: #fff;
11+
border: none;
12+
border-radius: 8px;
13+
padding: 8px 16px;
14+
cursor: pointer;
15+
outline: none;
16+
transition: background-color 0.2s;
17+
}
18+
button:active,
19+
button:focus,
20+
button:hover {
21+
background-color: #333;
22+
}
23+
24+
code {
25+
font-family: monospace;
26+
}
27+
828
#app {
929
display: flex;
1030
flex-direction: column;
@@ -499,4 +519,45 @@ button.close-button:hover {
499519
width: 100%;
500520
overflow-x: auto;
501521
white-space: pre-wrap;
502-
}
522+
}
523+
524+
/* Welcome popup */
525+
.welcome {
526+
position: fixed;
527+
top: 0;
528+
left: 0;
529+
right: 0;
530+
bottom: 0;
531+
display: flex;
532+
align-items: center;
533+
justify-content: center;
534+
z-index: 1000;
535+
animation: fadeIn 0.3s ease-in;
536+
}
537+
538+
.welcome > div {
539+
background-color: white;
540+
border-radius: 8px;
541+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
542+
display: flex;
543+
flex-direction: column;
544+
padding: 30px;
545+
width: 450px;
546+
max-width: 90%;
547+
}
548+
549+
.welcome h2 {
550+
color: #342267;
551+
margin-bottom: 16px;
552+
font-size: 24px;
553+
}
554+
555+
.welcome p {
556+
color: #444;
557+
line-height: 1.5;
558+
margin-bottom: 24px;
559+
}
560+
561+
.welcome button {
562+
margin-left: auto;
563+
}

test/components/Welcome.test.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { fireEvent, render } from '@testing-library/react'
2+
import React from 'react'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import Welcome from '../../src/components/Welcome.js'
5+
6+
describe('Welcome Component', () => {
7+
it('renders welcome content', () => {
8+
const onClose = vi.fn()
9+
const { getByRole, getByText } = render(<Welcome onClose={onClose} />)
10+
11+
expect(getByText('npx hyperparam')).toBeDefined()
12+
expect(getByText('Got it')).toBeDefined()
13+
const button = getByRole('button')
14+
expect(button).toBeDefined()
15+
expect(button.textContent).toBe('Got it')
16+
})
17+
18+
it('calls onClose when button is clicked', () => {
19+
const onClose = vi.fn()
20+
const { getByRole } = render(<Welcome onClose={onClose} />)
21+
22+
fireEvent.click(getByRole('button'))
23+
expect(onClose).toHaveBeenCalledTimes(1)
24+
})
25+
26+
it('calls onClose when clicking outside the popup', () => {
27+
const onClose = vi.fn()
28+
const { container } = render(<Welcome onClose={onClose} />)
29+
30+
// Find the backdrop element
31+
const backdropElement = container.querySelector('.welcome')
32+
expect(backdropElement).toBeDefined()
33+
34+
if (backdropElement) {
35+
fireEvent.click(backdropElement)
36+
expect(onClose).toHaveBeenCalledTimes(1)
37+
}
38+
})
39+
40+
it('does not call onClose when clicking inside the popup', () => {
41+
const onClose = vi.fn()
42+
const { getByText } = render(<Welcome onClose={onClose} />)
43+
44+
// Find and click on an element inside the popup content
45+
const paragraphElement = getByText('Supported file types include Parquet, CSV, JSON, Markdown, and Text.')
46+
fireEvent.click(paragraphElement)
47+
48+
expect(onClose).not.toHaveBeenCalled()
49+
})
50+
51+
it('calls onClose when pressing Escape key', () => {
52+
const onClose = vi.fn()
53+
render(<Welcome onClose={onClose} />)
54+
55+
// Simulate pressing the Escape key
56+
fireEvent.keyDown(window, { key: 'Escape' })
57+
expect(onClose).toHaveBeenCalledTimes(1)
58+
})
59+
60+
it('does not call onClose when pressing other keys', () => {
61+
const onClose = vi.fn()
62+
render(<Welcome onClose={onClose} />)
63+
64+
// Simulate pressing a different key
65+
fireEvent.keyDown(window, { key: 'Enter' })
66+
expect(onClose).not.toHaveBeenCalled()
67+
})
68+
})

0 commit comments

Comments
 (0)