Skip to content

Commit ebb31d8

Browse files
committed
feat(mock): phase 5o stage ui
1 parent 857a0b0 commit ebb31d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+9497
-177
lines changed
Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1+
import React from 'react'
12
import { useSession } from './hooks/useSession'
23
import { AppShell } from './components/layout/AppShell'
34
import { ConnectionScreen } from './components/layout/ConnectionScreen'
5+
import { ErrorBoundary } from './components/common/ErrorBoundary'
6+
import { KeyboardShortcuts } from './components/common/KeyboardShortcuts'
7+
import { Modal } from './components/modals/Modal'
48
import './styles/discord-theme.css'
59
import './styles/globals.css'
610

711
export default function App() {
8-
const { isConnected, sessionId } = useSession()
12+
const { isConnected, sessionId, activeModal, submitModal, closeModal } = useSession()
913

1014
// Show connection screen if not connected
1115
if (!isConnected || !sessionId) {
12-
return <ConnectionScreen />
16+
return (
17+
<ErrorBoundary>
18+
<ConnectionScreen />
19+
</ErrorBoundary>
20+
)
21+
}
22+
23+
// Handle modal submission
24+
const handleModalSubmit = async (customId: string, components: Parameters<typeof submitModal>[1]) => {
25+
await submitModal(customId, components)
26+
closeModal()
1327
}
1428

1529
// Show main app shell when connected
16-
return <AppShell />
30+
return (
31+
<ErrorBoundary>
32+
<KeyboardShortcuts />
33+
<AppShell />
34+
{activeModal && (
35+
<Modal
36+
modal={activeModal.modal}
37+
onClose={closeModal}
38+
onSubmit={handleModalSubmit}
39+
/>
40+
)}
41+
</ErrorBoundary>
42+
)
1743
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.picker {
2+
position: absolute;
3+
bottom: 100%;
4+
left: 0;
5+
margin-bottom: 4px;
6+
padding: 8px;
7+
background: var(--background-floating);
8+
border-radius: 8px;
9+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.24);
10+
z-index: 1000;
11+
}
12+
13+
.grid {
14+
display: grid;
15+
grid-template-columns: repeat(5, 1fr);
16+
gap: 4px;
17+
width: 180px;
18+
}
19+
20+
.emoji {
21+
display: flex;
22+
align-items: center;
23+
justify-content: center;
24+
width: 32px;
25+
height: 32px;
26+
padding: 0;
27+
font-size: 20px;
28+
line-height: 1;
29+
background: transparent;
30+
border: none;
31+
border-radius: 4px;
32+
cursor: pointer;
33+
transition: background-color 0.1s ease;
34+
}
35+
36+
.emoji:hover {
37+
background: var(--background-modifier-hover);
38+
}
39+
40+
.emoji:focus {
41+
outline: none;
42+
}
43+
44+
.emoji:focus-visible {
45+
outline: 2px solid var(--brand-500);
46+
outline-offset: -2px;
47+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect, useRef } from 'react'
2+
import styles from './EmojiPicker.module.css'
3+
4+
interface EmojiPickerProps {
5+
onSelect: (emoji: string) => void
6+
onClose: () => void
7+
}
8+
9+
// Common emoji set for MVP
10+
const EMOJI_LIST = [
11+
// Reactions
12+
'👍', '👎', '❤️', '🔥', '🎉',
13+
'😂', '😢', '😮', '😡', '🤔',
14+
// Common
15+
'👀', '💯', '✅', '❌', '⭐',
16+
'🙏', '💪', '🚀', '💡', '📌',
17+
// Faces
18+
'😊', '😎', '🤣', '😍', '🥳',
19+
'😴', '🤯', '🥺', '😤', '🤝'
20+
]
21+
22+
export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
23+
const pickerRef = useRef<HTMLDivElement>(null)
24+
25+
// Close on click outside
26+
useEffect(() => {
27+
const handleClickOutside = (event: MouseEvent) => {
28+
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
29+
onClose()
30+
}
31+
}
32+
33+
// Use capture phase to handle click before it bubbles
34+
document.addEventListener('mousedown', handleClickOutside, true)
35+
return () => document.removeEventListener('mousedown', handleClickOutside, true)
36+
}, [onClose])
37+
38+
// Close on Escape
39+
useEffect(() => {
40+
const handleKeyDown = (event: KeyboardEvent) => {
41+
if (event.key === 'Escape') {
42+
onClose()
43+
}
44+
}
45+
46+
document.addEventListener('keydown', handleKeyDown)
47+
return () => document.removeEventListener('keydown', handleKeyDown)
48+
}, [onClose])
49+
50+
return (
51+
<div ref={pickerRef} className={styles.picker} role="dialog" aria-label="Emoji picker">
52+
<div className={styles.grid}>
53+
{EMOJI_LIST.map((emoji) => (
54+
<button
55+
key={emoji}
56+
className={styles.emoji}
57+
onClick={() => onSelect(emoji)}
58+
title={emoji}
59+
>
60+
{emoji}
61+
</button>
62+
))}
63+
</div>
64+
</div>
65+
)
66+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
.container {
2+
display: flex;
3+
align-items: center;
4+
justify-content: center;
5+
min-height: 100vh;
6+
background: var(--background-primary);
7+
padding: 20px;
8+
}
9+
10+
.content {
11+
max-width: 500px;
12+
text-align: center;
13+
background: var(--background-secondary);
14+
border-radius: 8px;
15+
padding: 40px;
16+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
17+
}
18+
19+
.icon {
20+
color: var(--status-danger);
21+
margin-bottom: 16px;
22+
}
23+
24+
.title {
25+
font-size: 24px;
26+
font-weight: 600;
27+
color: var(--header-primary);
28+
margin: 0 0 12px 0;
29+
}
30+
31+
.message {
32+
font-size: 14px;
33+
color: var(--text-muted);
34+
margin: 0 0 24px 0;
35+
line-height: 1.5;
36+
}
37+
38+
.details {
39+
text-align: left;
40+
background: var(--background-tertiary);
41+
border-radius: 4px;
42+
margin-bottom: 24px;
43+
overflow: hidden;
44+
}
45+
46+
.summary {
47+
padding: 12px 16px;
48+
cursor: pointer;
49+
font-size: 13px;
50+
font-weight: 500;
51+
color: var(--text-normal);
52+
user-select: none;
53+
}
54+
55+
.summary:hover {
56+
background: var(--background-modifier-hover);
57+
}
58+
59+
.errorStack {
60+
padding: 0 16px 16px 16px;
61+
}
62+
63+
.errorName {
64+
font-size: 13px;
65+
font-weight: 600;
66+
color: var(--status-danger);
67+
margin-bottom: 8px;
68+
}
69+
70+
.componentStackLabel {
71+
font-size: 12px;
72+
font-weight: 600;
73+
color: var(--text-muted);
74+
margin-top: 16px;
75+
margin-bottom: 8px;
76+
}
77+
78+
.stack {
79+
font-size: 11px;
80+
font-family: 'Consolas', 'Monaco', monospace;
81+
color: var(--text-muted);
82+
background: var(--background-primary);
83+
padding: 12px;
84+
border-radius: 4px;
85+
overflow-x: auto;
86+
white-space: pre-wrap;
87+
word-break: break-all;
88+
margin: 0;
89+
max-height: 200px;
90+
overflow-y: auto;
91+
}
92+
93+
.button {
94+
background: var(--brand-500);
95+
color: white;
96+
border: none;
97+
border-radius: 4px;
98+
padding: 12px 24px;
99+
font-size: 14px;
100+
font-weight: 500;
101+
cursor: pointer;
102+
transition: background 0.15s ease;
103+
}
104+
105+
.button:hover {
106+
background: var(--brand-560);
107+
}
108+
109+
.button:active {
110+
background: var(--brand-600);
111+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Component, type ReactNode, type ErrorInfo } from 'react'
2+
import styles from './ErrorBoundary.module.css'
3+
4+
interface ErrorBoundaryProps {
5+
children: ReactNode
6+
fallback?: ReactNode
7+
}
8+
9+
interface ErrorBoundaryState {
10+
hasError: boolean
11+
error: Error | null
12+
errorInfo: ErrorInfo | null
13+
}
14+
15+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
16+
constructor(props: ErrorBoundaryProps) {
17+
super(props)
18+
this.state = {
19+
hasError: false,
20+
error: null,
21+
errorInfo: null
22+
}
23+
}
24+
25+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
26+
return { hasError: true, error }
27+
}
28+
29+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
30+
this.setState({ errorInfo })
31+
console.error('ErrorBoundary caught an error:', error, errorInfo)
32+
}
33+
34+
handleReset = (): void => {
35+
this.setState({
36+
hasError: false,
37+
error: null,
38+
errorInfo: null
39+
})
40+
}
41+
42+
render(): ReactNode {
43+
if (this.state.hasError) {
44+
if (this.props.fallback) {
45+
return this.props.fallback
46+
}
47+
48+
return (
49+
<div className={styles.container}>
50+
<div className={styles.content}>
51+
<div className={styles.icon}>
52+
<WarningIcon />
53+
</div>
54+
<h1 className={styles.title}>Something went wrong</h1>
55+
<p className={styles.message}>
56+
An unexpected error occurred. This has been logged for investigation.
57+
</p>
58+
59+
{process.env.NODE_ENV === 'development' && this.state.error && (
60+
<details className={styles.details}>
61+
<summary className={styles.summary}>Error Details</summary>
62+
<div className={styles.errorStack}>
63+
<div className={styles.errorName}>{this.state.error.name}: {this.state.error.message}</div>
64+
{this.state.error.stack && (
65+
<pre className={styles.stack}>{this.state.error.stack}</pre>
66+
)}
67+
{this.state.errorInfo?.componentStack && (
68+
<>
69+
<div className={styles.componentStackLabel}>Component Stack:</div>
70+
<pre className={styles.stack}>{this.state.errorInfo.componentStack}</pre>
71+
</>
72+
)}
73+
</div>
74+
</details>
75+
)}
76+
77+
<button className={styles.button} onClick={this.handleReset}>
78+
Try Again
79+
</button>
80+
</div>
81+
</div>
82+
)
83+
}
84+
85+
return this.props.children
86+
}
87+
}
88+
89+
function WarningIcon() {
90+
return (
91+
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
92+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-2zm0-4V7h2v6h-2z" />
93+
</svg>
94+
)
95+
}

0 commit comments

Comments
 (0)