Skip to content

Commit d53a2d4

Browse files
committed
custom alert, more animations and theoretical mobile support
1 parent f7c2f08 commit d53a2d4

11 files changed

Lines changed: 396 additions & 136 deletions

src/App.jsx

Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import LoginPage from './pages/LoginPage';
33
import Dashboard from './pages/DashboardPage';
44
import SpreadsheetSelector from './components/SpreadsheetSelector';
55
import LoadingSpinner from './components/LoadingSpinner';
6+
import { AlertProvider } from './components/AlertContext';
67
import {
78
initializeGoogleAuth,
89
clearTokens,
@@ -11,7 +12,7 @@ import {
1112
getSavedSession
1213
} from './utils/googleAuth';
1314

14-
function App() {
15+
export default function App() {
1516
const [user, setUser] = useState(null);
1617
const [loading, setLoading] = useState(true);
1718
const [needsSpreadsheetSelection, setNeedsSpreadsheetSelection] = useState(false);
@@ -84,82 +85,26 @@ function App() {
8485
return <LoadingSpinner />;
8586
}
8687

87-
if (!user) {
88-
return <LoginPage onSignIn={handleSignIn} />;
89-
}
90-
91-
if (needsSpreadsheetSelection) {
92-
return (
93-
<SpreadsheetSelector
94-
user={user}
95-
onSelected={handleSpreadsheetSelected}
96-
onCancel={handleSpreadsheetCancelled}
97-
onSignOut={handleSignOut}
98-
/>
99-
);
100-
}
101-
102-
if (cancelled) {
103-
return (
104-
<CancelledScreen
105-
user={user}
106-
onRetry={handleRetrySelection}
107-
onSignOut={handleSignOut}
108-
/>
109-
);
110-
}
111-
112-
return <Dashboard user={user} onSignOut={handleSignOut} />;
113-
}
114-
115-
function CancelledScreen({ user, onRetry, onSignOut }) {
11688
return (
117-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
118-
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full">
119-
<div className="flex items-center justify-between mb-6">
120-
<div className="flex items-center gap-3">
121-
<img
122-
src={user.picture}
123-
alt="Profile"
124-
className="w-12 h-12 rounded-full border-2 border-blue-600"
125-
referrerPolicy="no-referrer"
126-
/>
127-
<div>
128-
<p className="font-semibold text-gray-800">{user.name}</p>
129-
<p className="text-sm text-gray-600">{user.email}</p>
130-
</div>
131-
</div>
132-
</div>
133-
134-
<div className="text-center mb-8">
135-
<div className="w-16 h-16 bg-gray-400 rounded-full flex items-center justify-center mx-auto mb-4">
136-
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
138-
</svg>
139-
</div>
140-
<h1 className="text-2xl font-bold text-gray-800 mb-2">Selection Cancelled</h1>
141-
<p className="text-gray-600">
142-
You need to select the purchasing spreadsheet to continue using the application.
143-
</p>
144-
</div>
145-
146-
<div className="space-y-3">
147-
<button
148-
onClick={onRetry}
149-
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg transition duration-200"
150-
>
151-
Select Spreadsheet
152-
</button>
153-
<button
154-
onClick={onSignOut}
155-
className="w-full bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-6 rounded-lg transition duration-200"
156-
>
157-
Sign Out
158-
</button>
159-
</div>
160-
</div>
161-
</div>
89+
<AlertProvider>
90+
{!user ? (
91+
<LoginPage onSignIn={handleSignIn} />
92+
) : needsSpreadsheetSelection ? (
93+
<SpreadsheetSelector
94+
user={user}
95+
onSelected={handleSpreadsheetSelected}
96+
onCancel={handleSpreadsheetCancelled}
97+
onSignOut={handleSignOut}
98+
/>
99+
) : cancelled ? (
100+
<CancelledScreen
101+
user={user}
102+
onRetry={handleRetrySelection}
103+
onSignOut={handleSignOut}
104+
/>
105+
) : (
106+
<Dashboard user={user} onSignOut={handleSignOut} />
107+
)}
108+
</AlertProvider>
162109
);
163-
}
164-
165-
export default App;
110+
}

src/components/AlertContext.jsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createContext, useContext, useState } from 'react';
2+
import CustomAlert from './CustomAlert';
3+
4+
const AlertContext = createContext();
5+
6+
export const useAlert = () => {
7+
const context = useContext(AlertContext);
8+
if (!context) {
9+
throw new Error('useAlert must be used within AlertProvider');
10+
}
11+
return context;
12+
};
13+
14+
export const AlertProvider = ({ children }) => {
15+
const [alertConfig, setAlertConfig] = useState(null);
16+
17+
const showAlert = (message, options = {}) => {
18+
return new Promise((resolve) => {
19+
setAlertConfig({
20+
message,
21+
type: options.type || 'info', // 'info', 'warning', 'error', 'success'
22+
confirmText: options.confirmText || 'OK',
23+
cancelText: options.cancelText,
24+
onConfirm: () => {
25+
setAlertConfig(null);
26+
resolve(true);
27+
},
28+
onCancel: () => {
29+
setAlertConfig(null);
30+
resolve(false);
31+
}
32+
});
33+
});
34+
};
35+
36+
const showConfirm = (message, options = {}) => {
37+
return showAlert(message, {
38+
type: 'warning',
39+
confirmText: options.confirmText || 'Confirm',
40+
cancelText: options.cancelText || 'Cancel',
41+
...options
42+
});
43+
};
44+
45+
const showError = (message, options = {}) => {
46+
return showAlert(message, {
47+
type: 'error',
48+
confirmText: 'OK',
49+
...options
50+
});
51+
};
52+
53+
const showSuccess = (message, options = {}) => {
54+
return showAlert(message, {
55+
type: 'success',
56+
confirmText: 'OK',
57+
...options
58+
});
59+
};
60+
61+
return (
62+
<AlertContext.Provider value={{ showAlert, showConfirm, showError, showSuccess }}>
63+
{children}
64+
{alertConfig && <CustomAlert {...alertConfig} />}
65+
</AlertContext.Provider>
66+
);
67+
};

src/components/CustomAlert.jsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useEffect, useState } from 'react';
2+
import { AlertCircle, CheckCircle, Info, XCircle } from 'lucide-react';
3+
4+
export default function CustomAlert({
5+
message,
6+
type = 'info',
7+
confirmText = 'OK',
8+
cancelText,
9+
onConfirm,
10+
onCancel
11+
}) {
12+
const [isClosing, setIsClosing] = useState(false);
13+
const [isVisible, setIsVisible] = useState(false);
14+
15+
useEffect(() => {
16+
// Prevent background scroll
17+
document.body.style.overflow = 'hidden';
18+
19+
// Small delay for animation
20+
setTimeout(() => setIsVisible(true), 10);
21+
22+
return () => {
23+
document.body.style.overflow = '';
24+
};
25+
}, []);
26+
27+
// Handle escape key
28+
useEffect(() => {
29+
const handleEscape = (e) => {
30+
if (e.key === 'Escape' && cancelText) {
31+
handleCancel();
32+
} else if (e.key === 'Escape' && !cancelText) {
33+
handleConfirm();
34+
}
35+
};
36+
37+
window.addEventListener('keydown', handleEscape);
38+
return () => window.removeEventListener('keydown', handleEscape);
39+
}, [cancelText]);
40+
41+
const handleConfirm = () => {
42+
setIsClosing(true);
43+
setTimeout(() => {
44+
onConfirm?.();
45+
}, 200);
46+
};
47+
48+
const handleCancel = () => {
49+
setIsClosing(true);
50+
setTimeout(() => {
51+
onCancel?.();
52+
}, 200);
53+
};
54+
55+
const getIcon = () => {
56+
switch (type) {
57+
case 'error':
58+
return <XCircle className="w-12 h-12 text-red-600" />;
59+
case 'warning':
60+
return <AlertCircle className="w-12 h-12 text-orange-600" />;
61+
case 'success':
62+
return <CheckCircle className="w-12 h-12 text-green-600" />;
63+
default:
64+
return <Info className="w-12 h-12 text-blue-600" />;
65+
}
66+
};
67+
68+
const getColors = () => {
69+
switch (type) {
70+
case 'error':
71+
return {
72+
bg: 'bg-red-50',
73+
border: 'border-red-200',
74+
button: 'bg-red-600 hover:bg-red-700'
75+
};
76+
case 'warning':
77+
return {
78+
bg: 'bg-orange-50',
79+
border: 'border-orange-200',
80+
button: 'bg-orange-600 hover:bg-orange-700'
81+
};
82+
case 'success':
83+
return {
84+
bg: 'bg-green-50',
85+
border: 'border-green-200',
86+
button: 'bg-green-600 hover:bg-green-700'
87+
};
88+
default:
89+
return {
90+
bg: 'bg-blue-50',
91+
border: 'border-blue-200',
92+
button: 'bg-blue-600 hover:bg-blue-700'
93+
};
94+
}
95+
};
96+
97+
const colors = getColors();
98+
99+
return (
100+
<div
101+
className={`fixed inset-0 bg-black z-[9999] flex items-center justify-center p-4 transition-opacity duration-200 ${
102+
isClosing ? 'bg-opacity-0' : isVisible ? 'bg-opacity-50' : 'bg-opacity-0'
103+
}`}
104+
onClick={cancelText ? handleCancel : undefined}
105+
>
106+
<div
107+
className={`bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 transition-all duration-200 ${
108+
isClosing
109+
? 'opacity-0 scale-95'
110+
: isVisible
111+
? 'opacity-100 scale-100'
112+
: 'opacity-0 scale-95'
113+
}`}
114+
onClick={(e) => e.stopPropagation()}
115+
>
116+
{/* Icon */}
117+
<div className="flex justify-center mb-4">
118+
{getIcon()}
119+
</div>
120+
121+
{/* Message */}
122+
<div className={`${colors.bg} border ${colors.border} rounded-lg p-4 mb-6`}>
123+
<p className="text-gray-800 text-center whitespace-pre-wrap break-words">
124+
{message}
125+
</p>
126+
</div>
127+
128+
{/* Buttons */}
129+
<div className="flex flex-col-reverse sm:flex-row gap-3">
130+
{cancelText && (
131+
<button
132+
onClick={handleCancel}
133+
className="w-full sm:w-auto sm:flex-1 px-6 py-3 bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold rounded-lg transition duration-200 transform active:scale-95"
134+
>
135+
{cancelText}
136+
</button>
137+
)}
138+
<button
139+
onClick={handleConfirm}
140+
className={`w-full sm:w-auto sm:flex-1 px-6 py-3 text-white font-semibold rounded-lg transition duration-200 transform active:scale-95 ${colors.button}`}
141+
autoFocus
142+
>
143+
{confirmText}
144+
</button>
145+
</div>
146+
</div>
147+
</div>
148+
);
149+
}

0 commit comments

Comments
 (0)