Add network-based group invitations and fix AI popup

- Created plant logo SVG for Community Garden group
- Fixed AI popup to only appear for new users from invitations
- Added "Select from your network" button to invite form
- Implemented contact selection flow with prefilled data
- Fixed TypeScript build errors (Divider import, unused variable)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent f00efde726
commit 9a2170ec86
  1. 65
      public/community-garden-logo.svg
  2. 13
      public/groups.json
  3. 55
      src/components/invite/InviteForm.tsx
  4. 35
      src/pages/GroupDetailPage.tsx

@ -0,0 +1,65 @@
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Gradients for natural plant colors -->
<linearGradient id="leafGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4ade80;stop-opacity:1" />
<stop offset="50%" style="stop-color:#22c55e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16a34a;stop-opacity:1" />
</linearGradient>
<linearGradient id="stemGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#65a30d;stop-opacity:1" />
<stop offset="100%" style="stop-color:#4d7c0f;stop-opacity:1" />
</linearGradient>
<linearGradient id="soilGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#a3a3a3;stop-opacity:1" />
<stop offset="50%" style="stop-color:#737373;stop-opacity:1" />
<stop offset="100%" style="stop-color:#525252;stop-opacity:1" />
</linearGradient>
<radialGradient id="flowerGradient" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color:#fbbf24;stop-opacity:1" />
<stop offset="70%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#d97706;stop-opacity:1" />
</radialGradient>
</defs>
<!-- Background circle -->
<circle cx="32" cy="32" r="30" fill="url(#soilGradient)" stroke="#374151" stroke-width="2"/>
<!-- Soil/ground -->
<ellipse cx="32" cy="52" rx="28" ry="8" fill="#8b5cf6" opacity="0.2"/>
<!-- Main stem -->
<path d="M32 52 Q32 45 32 35 Q32 25 32 18" stroke="url(#stemGradient)" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Left branch -->
<path d="M32 30 Q28 28 24 26" stroke="url(#stemGradient)" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Right branch -->
<path d="M32 35 Q36 33 40 31" stroke="url(#stemGradient)" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Large leaf (left) -->
<path d="M24 26 Q18 22 16 18 Q18 16 22 18 Q26 22 24 26" fill="url(#leafGradient)" stroke="#16a34a" stroke-width="1"/>
<!-- Medium leaf (right) -->
<path d="M40 31 Q44 27 46 23 Q44 21 40 23 Q36 27 40 31" fill="url(#leafGradient)" stroke="#16a34a" stroke-width="1"/>
<!-- Top leaves -->
<path d="M32 18 Q28 14 24 12 Q26 10 30 12 Q34 16 32 18" fill="url(#leafGradient)" stroke="#16a34a" stroke-width="1"/>
<path d="M32 18 Q36 14 40 12 Q38 10 34 12 Q30 16 32 18" fill="url(#leafGradient)" stroke="#16a34a" stroke-width="1"/>
<!-- Small flower -->
<circle cx="32" cy="16" r="4" fill="url(#flowerGradient)" stroke="#d97706" stroke-width="1"/>
<circle cx="32" cy="16" r="2" fill="#fef3c7"/>
<!-- Leaf details (veins) -->
<path d="M20 20 Q22 18 24 22" stroke="#16a34a" stroke-width="0.5" fill="none" opacity="0.6"/>
<path d="M42 25 Q44 23 46 27" stroke="#16a34a" stroke-width="0.5" fill="none" opacity="0.6"/>
<path d="M28 14 Q30 12 32 16" stroke="#16a34a" stroke-width="0.5" fill="none" opacity="0.6"/>
<path d="M36 14 Q34 12 32 16" stroke="#16a34a" stroke-width="0.5" fill="none" opacity="0.6"/>
<!-- Small decorative elements (seeds/sprouts) -->
<circle cx="20" cy="48" r="1.5" fill="#22c55e" opacity="0.7"/>
<circle cx="44" cy="50" r="1.5" fill="#22c55e" opacity="0.7"/>
<circle cx="28" cy="54" r="1" fill="#4ade80" opacity="0.5"/>
<circle cx="36" cy="55" r="1" fill="#4ade80" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -12,6 +12,19 @@
"tags": ["nao", "genesis", "governance", "culture", "legal", "architecture"], "tags": ["nao", "genesis", "governance", "culture", "legal", "architecture"],
"image": "/naog1-butterfly-logo.svg" "image": "/naog1-butterfly-logo.svg"
}, },
{
"id": "8",
"name": "Anyville Community Garden",
"description": "A local community group dedicated to growing fresh produce, sharing gardening knowledge, and building neighborhood connections through sustainable urban agriculture. Join us for weekly garden workdays, seasonal harvests, and educational workshops.",
"memberCount": 32,
"memberIds": ["1", "3", "5", "7", "8", "9"],
"createdBy": "7",
"createdAt": "2023-03-15T10:00:00Z",
"updatedAt": "2024-12-20T14:30:00Z",
"isPrivate": true,
"tags": ["community", "gardening", "sustainability", "local", "education", "environment"],
"image": "/community-garden-logo.svg"
},
{ {
"id": "1", "id": "1",
"name": "React Developers", "name": "React Developers",

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { import {
Dialog, Dialog,
DialogTitle, DialogTitle,
@ -10,6 +10,7 @@ import {
Typography, Typography,
Avatar, Avatar,
useTheme, useTheme,
Divider,
} from '@mui/material'; } from '@mui/material';
import { import {
Business, Business,
@ -19,6 +20,7 @@ import {
Favorite, Favorite,
Home, Home,
PersonAdd, PersonAdd,
ContactPage,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { DEFAULT_RCARDS } from '../../types/notification'; import { DEFAULT_RCARDS } from '../../types/notification';
import type { Group } from '../../types/group'; import type { Group } from '../../types/group';
@ -27,7 +29,12 @@ interface InviteFormProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (inviteData: InviteFormData) => void; onSubmit: (inviteData: InviteFormData) => void;
onSelectFromNetwork: () => void;
group: Group; group: Group;
prefilledContact?: {
name: string;
email: string;
};
} }
export interface InviteFormData { export interface InviteFormData {
@ -54,7 +61,9 @@ const InviteForm: React.FC<InviteFormProps> = ({
open, open,
onClose, onClose,
onSubmit, onSubmit,
group onSelectFromNetwork,
group,
prefilledContact
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [formData, setFormData] = useState<InviteFormState>({ const [formData, setFormData] = useState<InviteFormState>({
@ -65,6 +74,17 @@ const InviteForm: React.FC<InviteFormProps> = ({
}); });
const [selectedRelationship, setSelectedRelationship] = useState<string>(''); const [selectedRelationship, setSelectedRelationship] = useState<string>('');
// Handle prefilled contact data
useEffect(() => {
if (prefilledContact) {
setFormData(prev => ({
...prev,
inviteeName: prefilledContact.name,
inviteeEmail: prefilledContact.email
}));
}
}, [prefilledContact]);
const getRCardIcon = (iconName: string) => { const getRCardIcon = (iconName: string) => {
const iconMap: Record<string, React.ReactElement> = { const iconMap: Record<string, React.ReactElement> = {
Business: <Business />, Business: <Business />,
@ -122,6 +142,37 @@ const InviteForm: React.FC<InviteFormProps> = ({
</DialogTitle> </DialogTitle>
<DialogContent sx={{ p: 3 }}> <DialogContent sx={{ p: 3 }}>
{/* Network Selection Option */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<Button
variant="outlined"
startIcon={<ContactPage />}
onClick={onSelectFromNetwork}
sx={{
borderRadius: 2,
textTransform: 'none',
py: 1.5,
px: 3,
borderColor: 'primary.main',
'&:hover': {
transform: 'translateY(-1px)',
boxShadow: theme.shadows[4],
},
}}
>
Select from your network
</Button>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Choose from your existing contacts to invite
</Typography>
</Box>
<Divider sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary">
or enter manually
</Typography>
</Divider>
{/* Basic Info */} {/* Basic Info */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>

@ -61,6 +61,7 @@ const GroupDetailPage = () => {
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
const [showInviteForm, setShowInviteForm] = useState(false); const [showInviteForm, setShowInviteForm] = useState(false);
const [userFirstName, setUserFirstName] = useState<string>(); const [userFirstName, setUserFirstName] = useState<string>();
const [selectedContact, setSelectedContact] = useState<{name: string; email: string} | undefined>();
useEffect(() => { useEffect(() => {
const loadGroupData = async () => { const loadGroupData = async () => {
@ -73,7 +74,6 @@ const GroupDetailPage = () => {
// Check if this is user's first visit to this group or came from invitation // Check if this is user's first visit to this group or came from invitation
const hasVisitedKey = `hasVisited_group_${groupId}`; const hasVisitedKey = `hasVisited_group_${groupId}`;
const hasVisited = localStorage.getItem(hasVisitedKey);
const fromInvite = searchParams.get('fromInvite') === 'true'; const fromInvite = searchParams.get('fromInvite') === 'true';
const newMember = searchParams.get('newMember') === 'true'; const newMember = searchParams.get('newMember') === 'true';
@ -82,8 +82,26 @@ const GroupDetailPage = () => {
if (firstName) { if (firstName) {
setUserFirstName(firstName); setUserFirstName(firstName);
} }
// Handle returning from contact selection
const selectedContactName = searchParams.get('selectedContactName');
const selectedContactEmail = searchParams.get('selectedContactEmail');
if (selectedContactName && selectedContactEmail) {
setSelectedContact({
name: selectedContactName,
email: selectedContactEmail
});
setShowInviteForm(true);
// Clean up selection parameters
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete('selectedContactName');
newSearchParams.delete('selectedContactEmail');
setSearchParams(newSearchParams);
}
if ((!hasVisited || fromInvite || newMember) && groupData) { // Only show AI assistant automatically for new users who just joined from an invitation
if ((fromInvite || newMember) && groupData) {
// Mark as visited and open AI assistant directly // Mark as visited and open AI assistant directly
localStorage.setItem(hasVisitedKey, 'true'); localStorage.setItem(hasVisitedKey, 'true');
setTimeout(() => setShowAIAssistant(true), 1000); // Small delay for better UX setTimeout(() => setShowAIAssistant(true), 1000); // Small delay for better UX
@ -187,6 +205,12 @@ const GroupDetailPage = () => {
navigate(`/invite?${inviteParams.toString()}`); navigate(`/invite?${inviteParams.toString()}`);
}; };
const handleSelectFromNetwork = () => {
// Navigate to contacts page with selection mode and return context
setShowInviteForm(false);
navigate(`/contacts?mode=select&returnTo=group-invite&groupId=${groupId}`);
};
const handleStartAIAssistant = (prompt?: string) => { const handleStartAIAssistant = (prompt?: string) => {
setInitialPrompt(prompt); setInitialPrompt(prompt);
setShowAIAssistant(true); setShowAIAssistant(true);
@ -916,9 +940,14 @@ const GroupDetailPage = () => {
{group && ( {group && (
<InviteForm <InviteForm
open={showInviteForm} open={showInviteForm}
onClose={() => setShowInviteForm(false)} onClose={() => {
setShowInviteForm(false);
setSelectedContact(undefined);
}}
onSubmit={handleInviteSubmit} onSubmit={handleInviteSubmit}
onSelectFromNetwork={handleSelectFromNetwork}
group={group} group={group}
prefilledContact={selectedContact}
/> />
)} )}
</Box> </Box>

Loading…
Cancel
Save