Skip to content

Commit 02bacce

Browse files
authored
Merge pull request #1976 from gtech-mulearn/dev
feat(dashboard): add QSeverse wallet connection banner and modal
2 parents 32cdfea + 4c3f50a commit 02bacce

File tree

7 files changed

+664
-42
lines changed

7 files changed

+664
-42
lines changed

src/ZustandProvider.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface UserProfile {
3636

3737
interface UserStore {
3838
userProfile: UserProfile;
39-
userInfo: UserInfo;
39+
userInfo: UserInfo;
4040
setUserProfile: (profile: UserProfile) => void;
4141
setUserInfo: (info: UserInfo) => void;
4242
updateUserProfile: (updates: Partial<UserProfile>) => void;
@@ -148,4 +148,49 @@ export const useStatStore = create<StatStore>(
148148
getStorage: () => localStorage, // Use localStorage for persistence
149149
}
150150
)
151-
);
151+
);
152+
153+
// QSeverse Connection Status Types
154+
// This store ONLY tracks whether the user has connected their wallet (for banner display)
155+
// It does NOT store which DID they use - that's handled by achievement cards on demand
156+
export type QseverseConnectionStatus = 'idle' | 'loading' | 'connected' | 'not_connected' | 'error';
157+
158+
interface QseverseStore {
159+
connectionStatus: QseverseConnectionStatus;
160+
hasCheckedConnection: boolean;
161+
setConnectionStatus: (status: QseverseConnectionStatus) => void;
162+
setHasCheckedConnection: (checked: boolean) => void;
163+
resetQseverseState: () => void;
164+
}
165+
166+
const initialQseverseState = {
167+
connectionStatus: 'idle' as QseverseConnectionStatus,
168+
hasCheckedConnection: false,
169+
};
170+
171+
export const useQseverseStore = create<QseverseStore>(
172+
//@ts-ignore
173+
persist(
174+
(set) => ({
175+
...initialQseverseState,
176+
setConnectionStatus: (status: QseverseConnectionStatus) =>
177+
set({ connectionStatus: status }),
178+
setHasCheckedConnection: (checked: boolean) =>
179+
set({ hasCheckedConnection: checked }),
180+
resetQseverseState: () =>
181+
set(initialQseverseState),
182+
}),
183+
{
184+
name: "qseverseStore",
185+
// @ts-ignore
186+
getStorage: () => localStorage,
187+
// Only persist connectionStatus, NOT hasCheckedConnection
188+
// This ensures API is called on each fresh page load
189+
partialize: (state: QseverseStore) => ({
190+
connectionStatus: state.connectionStatus,
191+
}),
192+
}
193+
)
194+
);
195+
196+
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
.alertBanner {
2+
display: flex;
3+
align-items: center;
4+
gap: 12px;
5+
padding: 12px 20px;
6+
border-radius: 50px;
7+
margin: 0;
8+
animation: slideIn 0.3s ease-out;
9+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
10+
position: relative;
11+
overflow: hidden;
12+
backdrop-filter: blur(10px);
13+
}
14+
15+
.alertBanner.animatingOut {
16+
animation: slideOut 0.3s ease-in forwards;
17+
}
18+
19+
@keyframes slideIn {
20+
from {
21+
opacity: 0;
22+
transform: translateY(-10px);
23+
}
24+
25+
to {
26+
opacity: 1;
27+
transform: translateY(0);
28+
}
29+
}
30+
31+
@keyframes slideOut {
32+
from {
33+
opacity: 1;
34+
transform: translateY(0);
35+
}
36+
37+
to {
38+
opacity: 0;
39+
transform: translateY(-10px);
40+
}
41+
}
42+
43+
/* Variant Styles */
44+
.info {
45+
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
46+
border: 1px solid #7dd3fc;
47+
color: #0c4a6e;
48+
}
49+
50+
.info .iconContainer i {
51+
color: #0284c7;
52+
}
53+
54+
.warning {
55+
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
56+
border: 1px solid #fbbf24;
57+
color: #78350f;
58+
}
59+
60+
.warning .iconContainer i {
61+
color: #d97706;
62+
}
63+
64+
.success {
65+
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
66+
border: 1px solid #6ee7b7;
67+
color: #064e3b;
68+
}
69+
70+
.success .iconContainer i {
71+
color: #059669;
72+
}
73+
74+
.error {
75+
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
76+
border: 1px solid #f87171;
77+
color: #7f1d1d;
78+
}
79+
80+
.error .iconContainer i {
81+
color: #dc2626;
82+
}
83+
84+
/* Icon Container */
85+
.iconContainer {
86+
display: flex;
87+
align-items: center;
88+
justify-content: center;
89+
flex-shrink: 0;
90+
}
91+
92+
.iconContainer i {
93+
font-size: 20px;
94+
display: flex;
95+
align-items: center;
96+
}
97+
98+
/* Content */
99+
.content {
100+
display: flex;
101+
flex-direction: column;
102+
gap: 2px;
103+
flex: 1;
104+
min-width: 0;
105+
}
106+
107+
.title {
108+
font-weight: 600;
109+
font-size: 14px;
110+
line-height: 1.4;
111+
}
112+
113+
.description {
114+
font-size: 13px;
115+
font-weight: 400;
116+
opacity: 0.85;
117+
line-height: 1.4;
118+
}
119+
120+
/* Actions */
121+
.actions {
122+
display: flex;
123+
align-items: center;
124+
gap: 8px;
125+
flex-shrink: 0;
126+
}
127+
128+
.actionButton {
129+
padding: 6px 14px;
130+
border-radius: 8px;
131+
font-size: 13px;
132+
font-weight: 600;
133+
cursor: pointer;
134+
transition: all 0.2s ease;
135+
border: none;
136+
white-space: nowrap;
137+
}
138+
139+
.info .actionButton {
140+
background: #0284c7;
141+
color: white;
142+
}
143+
144+
.info .actionButton:hover {
145+
background: #0369a1;
146+
}
147+
148+
.warning .actionButton {
149+
background: #d97706;
150+
color: white;
151+
}
152+
153+
.warning .actionButton:hover {
154+
background: #b45309;
155+
}
156+
157+
.success .actionButton {
158+
background: #059669;
159+
color: white;
160+
}
161+
162+
.success .actionButton:hover {
163+
background: #047857;
164+
}
165+
166+
.error .actionButton {
167+
background: #dc2626;
168+
color: white;
169+
}
170+
171+
.error .actionButton:hover {
172+
background: #b91c1c;
173+
}
174+
175+
.dismissButton {
176+
display: flex;
177+
align-items: center;
178+
justify-content: center;
179+
width: 28px;
180+
height: 28px;
181+
border-radius: 6px;
182+
border: none;
183+
background: rgba(0, 0, 0, 0.05);
184+
cursor: pointer;
185+
transition: all 0.2s ease;
186+
}
187+
188+
.dismissButton:hover {
189+
background: rgba(0, 0, 0, 0.1);
190+
}
191+
192+
.dismissButton i {
193+
font-size: 16px;
194+
display: flex;
195+
opacity: 0.7;
196+
}
197+
198+
/* Responsive */
199+
@media screen and (max-width: 768px) {
200+
.alertBanner {
201+
flex-wrap: wrap;
202+
padding: 10px 14px;
203+
gap: 8px;
204+
}
205+
206+
.content {
207+
flex: 1 1 calc(100% - 80px);
208+
order: 1;
209+
}
210+
211+
.iconContainer {
212+
order: 0;
213+
}
214+
215+
.actions {
216+
order: 2;
217+
margin-left: auto;
218+
}
219+
220+
.title {
221+
font-size: 13px;
222+
}
223+
224+
.description {
225+
font-size: 12px;
226+
}
227+
}
228+
229+
@media screen and (max-width: 480px) {
230+
.alertBanner {
231+
margin: 0 0.5rem;
232+
padding: 10px 12px;
233+
}
234+
235+
.content {
236+
flex: 1 1 100%;
237+
}
238+
239+
.actions {
240+
width: 100%;
241+
justify-content: flex-end;
242+
margin-top: 4px;
243+
}
244+
245+
.actionButton {
246+
flex: 1;
247+
text-align: center;
248+
}
249+
}

0 commit comments

Comments
 (0)