Skip to content

Commit 8c0eda4

Browse files
authored
LLSC-79-FAQ-Page (#44)
## Notion ticket link https://www.notion.so/uwblueprintexecs/FAQ-Page-22110f3fb1dc807ea15aca94bdc70f83 <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * faq page + soft deletion endpoint + active col <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent 5459c49 commit 8c0eda4

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed

backend/app/models/User.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class User(Base):
1717
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
1818
auth_id = Column(String, nullable=False)
1919
approved = Column(Boolean, default=False)
20+
active = Column(Boolean, nullable=False, default=True)
2021

2122
role = relationship("Role")
2223

backend/app/routes/user.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,18 @@ async def delete_user(
103103
raise http_ex
104104
except Exception as e:
105105
raise HTTPException(status_code=500, detail=str(e))
106+
107+
108+
# soft delete user
109+
@router.post("/{user_id}/deactivate")
110+
async def deactivate_user(
111+
user_id: str,
112+
user_service: UserService = Depends(get_user_service),
113+
):
114+
try:
115+
await user_service.soft_delete_user_by_id(user_id)
116+
return {"message": "User deactivated successfully"}
117+
except HTTPException as http_ex:
118+
raise http_ex
119+
except Exception as e:
120+
raise HTTPException(status_code=500, detail=str(e))

backend/app/services/implementations/user_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ async def delete_user_by_id(self, user_id: str):
114114
self.logger.error(f"Error deleting user {user_id}: {str(e)}")
115115
raise HTTPException(status_code=500, detail=str(e))
116116

117+
async def soft_delete_user_by_id(self, user_id: str):
118+
try:
119+
db_user = self.db.query(User).filter(User.id == UUID(user_id)).first()
120+
if not db_user:
121+
raise HTTPException(status_code=404, detail="User not found")
122+
123+
db_user.active = False
124+
self.db.commit()
125+
except ValueError:
126+
raise HTTPException(status_code=400, detail="Invalid user ID format")
127+
except HTTPException:
128+
raise
129+
except Exception as e:
130+
self.db.rollback()
131+
self.logger.error(f"Error deactivating user {user_id}: {str(e)}")
132+
raise HTTPException(status_code=500, detail=str(e))
133+
117134
async def get_user_id_by_auth_id(self, auth_id: str) -> str:
118135
"""Get user ID for a user by their Firebase auth_id"""
119136
user = self.db.query(User).filter(User.auth_id == auth_id).first()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""add active column to users
2+
3+
Revision ID: abcd1234active
4+
Revises: df571b763807
5+
Create Date: 2025-07-08
6+
"""
7+
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
revision = "abcd1234active"
12+
down_revision = "df571b763807"
13+
branch_labels = None
14+
depends_on = None
15+
16+
17+
def upgrade():
18+
op.add_column("users", sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()))
19+
20+
21+
def downgrade():
22+
op.drop_column("users", "active")

frontend/src/pages/faq.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React, { useState } from 'react';
2+
import { FiChevronDown, FiChevronUp } from 'react-icons/fi';
3+
import { COLORS } from '@/constants/form';
4+
import { useRouter } from 'next/router';
5+
6+
interface FAQItem {
7+
id: string;
8+
question: string;
9+
answer: string;
10+
actionButton?: {
11+
text: string;
12+
action: () => void;
13+
};
14+
}
15+
16+
const ACCENT_COLOR = '#056067';
17+
const BORDER_COLOR_EXPANDED = '#5F989D';
18+
const SHADOW_COLOR = '#B3CED1';
19+
20+
export default function FAQPage() {
21+
const [expandedFAQs, setExpandedFAQs] = useState<string[]>([]);
22+
const router = useRouter();
23+
24+
const faqData: FAQItem[] = [
25+
{
26+
id: 'contact-staff',
27+
question: 'How can I contact a staff member?',
28+
answer:
29+
'Click the button below to fill out a short form, it only takes a minute! Once submitted, a staff member will follow up via email within 2 business days to support your needs from [email protected].',
30+
actionButton: {
31+
text: 'Contact Us!',
32+
action: () => true,
33+
},
34+
},
35+
{
36+
id: 'become-volunteer',
37+
question: 'How can I apply to become a volunteer?',
38+
answer:
39+
"Complete the volunteer application to express your interest and confirm these details are correct. Once submitted, we'll follow up by email with next steps.",
40+
actionButton: {
41+
text: 'Become a volunteer!',
42+
action: () => router.push('/volunteer/intake'),
43+
},
44+
},
45+
{
46+
id: 'opt-out',
47+
question: 'How can I opt out of the First Connections program?',
48+
answer:
49+
'Your experience is important to us. Our volunteers are the most important part of First Connection. Volunteers are encouraged to take the time they need away from the program when they need it. By opting out you are removing yourself from the matching algorithm and cannot be connected with a potential participant.\n\nWhen you are ready to volunteer with us again, please sign back in and click the Opt In. You do not need to re-register or create a new profile. If you would like to talk with a staff member about your time away or remove yourself completely from the program please reach out, we are here to help.',
50+
actionButton: {
51+
text: 'Opt out',
52+
action: () => true,
53+
},
54+
},
55+
];
56+
57+
return (
58+
<div className="flex min-h-screen bg-white">
59+
<div className="flex-1 p-6">
60+
<div className="mx-auto" style={{ width: '620px' }}>
61+
<h1
62+
className="font-semibold"
63+
style={{
64+
color: COLORS.veniceBlue,
65+
fontSize: '36px',
66+
letterSpacing: '-0.5px',
67+
fontFamily: 'Open Sans, sans-serif',
68+
marginBottom: '48px',
69+
}}
70+
>
71+
Frequently asked questions
72+
</h1>
73+
74+
<div className="flex flex-col gap-4">
75+
{faqData.map((faq) => {
76+
const isOpen = expandedFAQs.includes(faq.id);
77+
return (
78+
<div
79+
key={faq.id}
80+
className="bg-white rounded-lg transition-all duration-200 border"
81+
style={{
82+
borderColor: isOpen ? BORDER_COLOR_EXPANDED : '#e5e7eb',
83+
borderWidth: 1,
84+
boxShadow: isOpen
85+
? `0 0 0 4px ${SHADOW_COLOR}, 0 1px 2px rgba(10,13,18,0.05)`
86+
: 'none',
87+
}}
88+
>
89+
<button
90+
onClick={() =>
91+
setExpandedFAQs((prev) =>
92+
isOpen ? prev.filter((id) => id !== faq.id) : [...prev, faq.id],
93+
)
94+
}
95+
className="w-full flex items-center justify-between py-4 px-5 bg-transparent border-none rounded-lg cursor-pointer text-left hover:bg-gray-50"
96+
>
97+
<span
98+
style={{
99+
color: COLORS.veniceBlue,
100+
fontSize: '18px',
101+
fontWeight: 600,
102+
fontFamily: 'Open Sans, sans-serif',
103+
}}
104+
>
105+
{faq.question}
106+
</span>
107+
{isOpen ? (
108+
<FiChevronUp color={COLORS.veniceBlue} size={24} />
109+
) : (
110+
<FiChevronDown color={COLORS.veniceBlue} size={24} />
111+
)}
112+
</button>
113+
{isOpen && (
114+
<div className="px-5 pb-4">
115+
<div
116+
className="mb-4 whitespace-pre-wrap"
117+
style={{
118+
color: COLORS.veniceBlue,
119+
fontSize: '16px',
120+
fontWeight: 400,
121+
fontFamily: 'Open Sans, sans-serif',
122+
}}
123+
>
124+
{faq.answer}
125+
</div>
126+
{faq.actionButton && (
127+
<button
128+
onClick={faq.actionButton.action}
129+
className="inline-flex items-center justify-center font-medium cursor-pointer text-white"
130+
style={{
131+
backgroundColor: ACCENT_COLOR,
132+
border: `1px solid ${ACCENT_COLOR}`,
133+
borderRadius: '8px',
134+
padding: '8px 24px',
135+
minHeight: '36px',
136+
boxShadow: '0 1px 2px rgba(10,13,18,0.05)',
137+
fontSize: '14px',
138+
fontFamily: 'Open Sans, sans-serif',
139+
}}
140+
>
141+
{faq.actionButton.text}
142+
</button>
143+
)}
144+
</div>
145+
)}
146+
</div>
147+
);
148+
})}
149+
</div>
150+
</div>
151+
</div>
152+
</div>
153+
);
154+
}

0 commit comments

Comments
 (0)