Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class User(Base):
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
auth_id = Column(String, nullable=False)
approved = Column(Boolean, default=False)
active = Column(Boolean, nullable=False, default=True)

role = relationship("Role")

Expand Down
15 changes: 15 additions & 0 deletions backend/app/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,18 @@ async def delete_user(
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# soft delete user
@router.post("/{user_id}/deactivate")
async def deactivate_user(
user_id: str,
user_service: UserService = Depends(get_user_service),
):
try:
await user_service.soft_delete_user_by_id(user_id)
return {"message": "User deactivated successfully"}
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
17 changes: 17 additions & 0 deletions backend/app/services/implementations/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ async def delete_user_by_id(self, user_id: str):
self.logger.error(f"Error deleting user {user_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

async def soft_delete_user_by_id(self, user_id: str):
try:
db_user = self.db.query(User).filter(User.id == UUID(user_id)).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")

db_user.active = False
self.db.commit()
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
except HTTPException:
raise
except Exception as e:
self.db.rollback()
self.logger.error(f"Error deactivating user {user_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

async def get_user_id_by_auth_id(self, auth_id: str) -> str:
"""Get user ID for a user by their Firebase auth_id"""
user = self.db.query(User).filter(User.auth_id == auth_id).first()
Expand Down
22 changes: 22 additions & 0 deletions backend/migrations/versions/abcd1234_add_active_column_to_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""add active column to users

Revision ID: abcd1234active
Revises: df571b763807
Create Date: 2025-07-08
"""

import sqlalchemy as sa
from alembic import op

revision = "abcd1234active"
down_revision = "df571b763807"
branch_labels = None
depends_on = None


def upgrade():
op.add_column("users", sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()))


def downgrade():
op.drop_column("users", "active")
154 changes: 154 additions & 0 deletions frontend/src/pages/faq.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { FiChevronDown, FiChevronUp } from 'react-icons/fi';
import { COLORS } from '@/constants/form';
import { useRouter } from 'next/router';

interface FAQItem {
id: string;
question: string;
answer: string;
actionButton?: {
text: string;
action: () => void;
};
}

const ACCENT_COLOR = '#056067';
const BORDER_COLOR_EXPANDED = '#5F989D';
const SHADOW_COLOR = '#B3CED1';

export default function FAQPage() {
const [expandedFAQs, setExpandedFAQs] = useState<string[]>([]);
const router = useRouter();

const faqData: FAQItem[] = [
{
id: 'contact-staff',
question: 'How can I contact a staff member?',
answer:
'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].',
actionButton: {
text: 'Contact Us!',
action: () => true,
},
},
{
id: 'become-volunteer',
question: 'How can I apply to become a volunteer?',
answer:
"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.",
actionButton: {
text: 'Become a volunteer!',
action: () => router.push('/volunteer/intake'),
},
},
{
id: 'opt-out',
question: 'How can I opt out of the First Connections program?',
answer:
'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.',
actionButton: {
text: 'Opt out',
action: () => true,
},
},
];

return (
<div className="flex min-h-screen bg-white">
<div className="flex-1 p-6">
<div className="mx-auto" style={{ width: '620px' }}>
<h1
className="font-semibold"
style={{
color: COLORS.veniceBlue,
fontSize: '36px',
letterSpacing: '-0.5px',
fontFamily: 'Open Sans, sans-serif',
marginBottom: '48px',
}}
>
Frequently asked questions
</h1>

<div className="flex flex-col gap-4">
{faqData.map((faq) => {
const isOpen = expandedFAQs.includes(faq.id);
return (
<div
key={faq.id}
className="bg-white rounded-lg transition-all duration-200 border"
style={{
borderColor: isOpen ? BORDER_COLOR_EXPANDED : '#e5e7eb',
borderWidth: 1,
boxShadow: isOpen
? `0 0 0 4px ${SHADOW_COLOR}, 0 1px 2px rgba(10,13,18,0.05)`
: 'none',
}}
>
<button
onClick={() =>
setExpandedFAQs((prev) =>
isOpen ? prev.filter((id) => id !== faq.id) : [...prev, faq.id],
)
}
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"
>
<span
style={{
color: COLORS.veniceBlue,
fontSize: '18px',
fontWeight: 600,
fontFamily: 'Open Sans, sans-serif',
}}
>
{faq.question}
</span>
{isOpen ? (
<FiChevronUp color={COLORS.veniceBlue} size={24} />
) : (
<FiChevronDown color={COLORS.veniceBlue} size={24} />
)}
</button>
{isOpen && (
<div className="px-5 pb-4">
<div
className="mb-4 whitespace-pre-wrap"
style={{
color: COLORS.veniceBlue,
fontSize: '16px',
fontWeight: 400,
fontFamily: 'Open Sans, sans-serif',
}}
>
{faq.answer}
</div>
{faq.actionButton && (
<button
onClick={faq.actionButton.action}
className="inline-flex items-center justify-center font-medium cursor-pointer text-white"
style={{
backgroundColor: ACCENT_COLOR,
border: `1px solid ${ACCENT_COLOR}`,
borderRadius: '8px',
padding: '8px 24px',
minHeight: '36px',
boxShadow: '0 1px 2px rgba(10,13,18,0.05)',
fontSize: '14px',
fontFamily: 'Open Sans, sans-serif',
}}
>
{faq.actionButton.text}
</button>
)}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}