Skip to content

Commit 6522d9e

Browse files
committed
Add feedback carousel and auth roles
1 parent 2eca915 commit 6522d9e

5 files changed

Lines changed: 191 additions & 87 deletions

File tree

firestore.rules

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@ service cloud.firestore {
1515
&& request.auth.token.email == "tripleuworld@gmail.com";
1616
}
1717

18-
match /users/{emailKey} {
19-
function normalizedEmail(email) {
20-
return email != null ? lower(email) : '';
21-
}
22-
23-
allow read, write: if request.auth != null
24-
&& normalizedEmail(request.auth.token.email) == emailKey;
18+
match /users/{userId} {
19+
allow read, write: if request.auth != null && request.auth.uid == userId;
2520
}
2621
}
2722
}

src/components/Header.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from 'react';
2-
import { NavLink } from 'react-router-dom';
2+
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
33
import { AnimatePresence, motion } from 'framer-motion';
44
import { signOut } from 'firebase/auth';
55
import { auth } from '../lib/firebase';
@@ -21,6 +21,8 @@ export default function Header() {
2121
const [open, setOpen] = useState(false);
2222
const [profileOpen, setProfileOpen] = useState(false);
2323
const { user } = useAuth();
24+
const navigate = useNavigate();
25+
const location = useLocation();
2426

2527
useEffect(() => {
2628
setProfileOpen(false);
@@ -51,6 +53,18 @@ export default function Header() {
5153
setOpen(false);
5254
};
5355

56+
const handleFeedbackShortcut = () => {
57+
setProfileOpen(false);
58+
setOpen(false);
59+
const trigger = () => window.dispatchEvent(new CustomEvent('openFeedbackModal'));
60+
if (location.pathname !== '/') {
61+
navigate('/');
62+
setTimeout(trigger, 150);
63+
} else {
64+
trigger();
65+
}
66+
};
67+
5468
return (
5569
<header className="sticky top-0 z-40 border-b border-white/5 bg-slate-950/80 backdrop-blur-xl">
5670
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-5">
@@ -110,6 +124,13 @@ export default function Header() {
110124
>
111125
Apps dashboard
112126
</NavLink>
127+
<button
128+
type="button"
129+
className="mt-2 w-full rounded-full border border-sky-500/40 px-3 py-2 font-semibold text-sky-300 hover:border-sky-300"
130+
onClick={handleFeedbackShortcut}
131+
>
132+
Share feedback
133+
</button>
113134
<button
114135
type="button"
115136
className="mt-2 w-full rounded-full bg-slate-800 px-3 py-2 font-semibold hover:bg-slate-700"

src/context/AuthContext.jsx

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,76 @@ import { onAuthStateChanged, signOut } from 'firebase/auth';
33
import { doc, getDoc, setDoc } from 'firebase/firestore';
44
import { auth, firestore } from '../lib/firebase';
55

6-
const AuthContext = createContext({ user: null, loading: true });
6+
const AuthContext = createContext({ user: null, loading: true, profile: null });
77

88
export function AuthProvider({ children }) {
99
const [user, setUser] = useState(null);
1010
const [loading, setLoading] = useState(true);
11+
const [profile, setProfile] = useState(null);
1112

1213
useEffect(() => {
13-
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
14-
if (firebaseUser?.email) {
14+
let cancelled = false;
15+
16+
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
17+
if (!firebaseUser) {
18+
setUser(null);
19+
setProfile(null);
20+
setLoading(false);
21+
return;
22+
}
23+
24+
const hydrateProfile = async () => {
1525
try {
16-
const emailKey = firebaseUser.email.toLowerCase();
17-
const userRef = doc(firestore, 'users', emailKey);
26+
const userRef = doc(firestore, 'users', firebaseUser.uid);
1827
const snapshot = await getDoc(userRef);
19-
if (snapshot.exists() && snapshot.data()?.banned) {
28+
let data;
29+
if (snapshot.exists()) {
30+
data = snapshot.data();
31+
} else {
32+
data = {
33+
email: firebaseUser.email || '',
34+
displayName: firebaseUser.displayName || '',
35+
banned: false,
36+
isAdmin: false,
37+
};
38+
await setDoc(userRef, data, { merge: true });
39+
}
40+
41+
if (data?.banned) {
2042
await signOut(auth);
21-
setUser(null);
22-
setLoading(false);
43+
if (!cancelled) {
44+
setUser(null);
45+
setProfile(null);
46+
setLoading(false);
47+
}
2348
return;
2449
}
50+
51+
if (!cancelled) {
52+
setUser(firebaseUser);
53+
setProfile(data);
54+
setLoading(false);
55+
}
2556
} catch (error) {
2657
console.error('Auth guard failed', error);
58+
if (!cancelled) {
59+
setUser(firebaseUser);
60+
setProfile(null);
61+
setLoading(false);
62+
}
2763
}
28-
}
29-
setUser(firebaseUser);
30-
setLoading(false);
31-
});
32-
return unsubscribe;
33-
}, []);
34-
35-
useEffect(() => {
36-
if (!user?.email) return undefined;
37-
let cancelled = false;
64+
};
3865

39-
const syncUserDoc = async () => {
40-
try {
41-
const emailKey = user.email.toLowerCase();
42-
const userRef = doc(firestore, 'users', emailKey);
43-
const snapshot = await getDoc(userRef);
44-
if (!snapshot.exists() && !cancelled) {
45-
await setDoc(userRef, {
46-
email: user.email,
47-
banned: false,
48-
isAdmin: false,
49-
});
50-
}
51-
} catch (error) {
52-
console.error('Unable to sync user record', error);
53-
}
54-
};
66+
hydrateProfile();
67+
});
5568

56-
syncUserDoc();
5769
return () => {
5870
cancelled = true;
71+
unsubscribe();
5972
};
60-
}, [user?.email]);
73+
}, []);
6174

62-
return <AuthContext.Provider value={{ user, loading }}>{children}</AuthContext.Provider>;
75+
return <AuthContext.Provider value={{ user, loading, profile }}>{children}</AuthContext.Provider>;
6376
}
6477

6578
export const useAuth = () => useContext(AuthContext);

src/pages/Apps.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const initialForm = {
2828
};
2929

3030
export default function Apps() {
31-
const { user, loading } = useAuth();
31+
const { user, loading, profile } = useAuth();
3232
const [approvedApps, setApprovedApps] = useState([]);
3333
const [pendingApps, setPendingApps] = useState([]);
3434
const [formValues, setFormValues] = useState(initialForm);
@@ -41,6 +41,8 @@ export default function Apps() {
4141
const [catalogReady, setCatalogReady] = useState(false);
4242
const [catalogLoading, setCatalogLoading] = useState(true);
4343

44+
const isAdmin = Boolean(profile?.isAdmin || user?.email === ADMIN_EMAIL);
45+
4446
useEffect(() => {
4547
setCatalogReady(false);
4648
setCatalogLoading(true);
@@ -52,7 +54,7 @@ export default function Apps() {
5254
});
5355

5456
let unsubPending = () => {};
55-
if (user?.email === ADMIN_EMAIL) {
57+
if (isAdmin) {
5658
const pendingQuery = query(appsRef, where('status', '==', 'pending'), orderBy('createdAt', 'asc'));
5759
unsubPending = onSnapshot(pendingQuery, (snapshot) => {
5860
setPendingApps(snapshot.docs.map((docSnap) => ({ id: docSnap.id, ...docSnap.data() })));
@@ -65,7 +67,7 @@ export default function Apps() {
6567
unsubApproved();
6668
unsubPending();
6769
};
68-
}, [user?.email]);
70+
}, [user?.email, isAdmin]);
6971

7072
const approvedAppCards = useMemo(
7173
() =>

0 commit comments

Comments
 (0)