Skip to content

Commit 694204a

Browse files
committed
Add organization management pages and update functionalities
Implemented pages for viewing, editing, and dashboard management of organizations. Integrated Prisma for database interactions and incorporated server-side session handling for authentication-based access control. Took 54 seconds
1 parent 6b7bebb commit 694204a

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { Button } from '@/components/ui/button'
6+
import { Input } from '@/components/ui/input'
7+
import { Loader2 } from 'lucide-react'
8+
import {getOrganization} from "@/app/organizations/organizationActions";
9+
10+
interface CreateOrganizationFormProps {
11+
userId: string
12+
}
13+
14+
export default function CreateOrganizationForm({ userId }: CreateOrganizationFormProps) {
15+
const [url, setUrl] = useState('')
16+
const [isLoading, setIsLoading] = useState(false)
17+
const router = useRouter()
18+
19+
const handleSubmit = async (e: React.FormEvent) => {
20+
e.preventDefault()
21+
setIsLoading(true)
22+
try {
23+
const organization = await getOrganization(url, userId)
24+
router.push(`/organizations/${organization.slug}`)
25+
} catch (error) {
26+
console.error('Error creating/fetching organization:', error)
27+
// Handle error (e.g., show error message to user)
28+
setIsLoading(false)
29+
}
30+
}
31+
32+
return (
33+
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
34+
<div>
35+
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
36+
Organization URL
37+
</label>
38+
<Input
39+
type="text"
40+
id="url"
41+
value={url}
42+
onChange={(e) => setUrl(e.target.value)}
43+
required
44+
placeholder="Enter organization URL"
45+
/>
46+
</div>
47+
<Button type="submit" disabled={isLoading}>
48+
{isLoading ? (
49+
<>
50+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
51+
Creating...
52+
</>
53+
) : (
54+
'Create Organization'
55+
)}
56+
</Button>
57+
</form>
58+
)
59+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Organization } from '@prisma/client'
5+
import { updateOrganization } from '@/app/organizations/organizationActions'
6+
7+
interface OrganizationInfoProps {
8+
organization: Organization
9+
isOwner: boolean
10+
}
11+
12+
export default function OrganizationInfo({ organization, isOwner }: OrganizationInfoProps) {
13+
const [isEditing, setIsEditing] = useState(false)
14+
const [editedOrg, setEditedOrg] = useState(organization)
15+
16+
const handleEdit = async (e: React.FormEvent) => {
17+
e.preventDefault()
18+
try {
19+
const updatedOrg = await updateOrganization(organization.id, editedOrg)
20+
setEditedOrg(updatedOrg)
21+
setIsEditing(false)
22+
} catch (error) {
23+
console.error('Error updating organization:', error)
24+
}
25+
}
26+
27+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
28+
const { name, value } = e.target
29+
setEditedOrg((prev) => ({ ...prev, [name]: value }))
30+
}
31+
32+
if (isEditing) {
33+
return (
34+
<form onSubmit={handleEdit} className="space-y-4">
35+
<div>
36+
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label>
37+
<input
38+
type="text"
39+
id="name"
40+
name="name"
41+
value={editedOrg.name}
42+
onChange={handleInputChange}
43+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
44+
/>
45+
</div>
46+
<div>
47+
<label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
48+
<textarea
49+
id="description"
50+
name="description"
51+
value={editedOrg.description || ''}
52+
onChange={handleInputChange}
53+
rows={3}
54+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
55+
></textarea>
56+
</div>
57+
<div>
58+
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
59+
<input
60+
type="email"
61+
id="email"
62+
name="email"
63+
value={editedOrg.email || ''}
64+
onChange={handleInputChange}
65+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
66+
/>
67+
</div>
68+
<div>
69+
<label htmlFor="telephone" className="block text-sm font-medium text-gray-700">Telephone</label>
70+
<input
71+
type="tel"
72+
id="telephone"
73+
name="telephone"
74+
value={editedOrg.telephone || ''}
75+
onChange={handleInputChange}
76+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
77+
/>
78+
</div>
79+
<button type="submit" className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
80+
Save
81+
</button>
82+
</form>
83+
)
84+
}
85+
86+
return (
87+
<div className="space-y-4">
88+
<p className="text-gray-700">{organization.description}</p>
89+
<p className="text-gray-700">Email: {organization.email}</p>
90+
<p className="text-gray-700">Phone: {organization.telephone}</p>
91+
{isOwner && (
92+
<button
93+
onClick={() => setIsEditing(true)}
94+
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
95+
>
96+
Edit Organization
97+
</button>
98+
)}
99+
</div>
100+
)
101+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { prisma } from '@/lib/prisma'
2+
import { notFound } from 'next/navigation'
3+
import { getServerSession } from 'next-auth/next'
4+
import { authOptions } from "@/lib/auth"
5+
import Link from 'next/link'
6+
7+
export default async function OrganizationDashboard({ params }: { params: { slug: string } }) {
8+
const session = await getServerSession(authOptions)
9+
const organization = await prisma.organization.findUnique({
10+
where: { slug: params.slug },
11+
include: { owner: true }
12+
})
13+
14+
if (!organization) {
15+
notFound()
16+
}
17+
18+
const isOwner = session?.user?.id === organization.ownerId
19+
20+
if (!isOwner) {
21+
return <div>You do not have permission to view this dashboard.</div>
22+
}
23+
24+
return (
25+
<div className="container mx-auto px-4 py-8">
26+
<h1 className="text-3xl font-bold mb-6">{organization.name} Dashboard</h1>
27+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
28+
<div>
29+
<h2 className="text-2xl font-semibold mb-4">Quick Actions</h2>
30+
<div className="space-y-2">
31+
<Link href={`/organizations/${organization.url}/edit`}>
32+
<a className="block p-2 bg-blue-100 hover:bg-blue-200 rounded">Edit Organization Info</a>
33+
</Link>
34+
<Link href={`/organizations/${organization.url}/proposals/new`}>
35+
<a className="block p-2 bg-green-100 hover:bg-green-200 rounded">Create New Proposal</a>
36+
</Link>
37+
</div>
38+
</div>
39+
<div>
40+
<h2 className="text-2xl font-semibold mb-4">Organization Stats</h2>
41+
{/* Add organization statistics here */}
42+
</div>
43+
</div>
44+
</div>
45+
)
46+
}

app/organizations/[slug]/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { prisma } from '@/lib/prisma'
2+
import { notFound } from 'next/navigation'
3+
import OrganizationInfo from '../[slug]/OrganizationInfo'
4+
import { getServerSession } from 'next-auth/next'
5+
import { authOptions } from "@/lib/auth"
6+
7+
export default async function OrganizationPage({ params }: { params: { slug: string } }) {
8+
const session = await getServerSession(authOptions)
9+
const organization = await prisma.organization.findUnique({
10+
where: { slug: params.slug },
11+
include: { owner: true }
12+
})
13+
14+
if (!organization) {
15+
notFound()
16+
}
17+
18+
const isOwner = session?.user?.id === organization.ownerId
19+
20+
return (
21+
<div className="container mx-auto px-4 py-8">
22+
<h1 className="text-3xl font-bold mb-6">{organization.name}</h1>
23+
<OrganizationInfo organization={organization} isOwner={isOwner} />
24+
</div>
25+
)
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
5+
6+
7+
import { getOrCreateOrganizationFromUrl } from "@/lib/agents/researcher/organizationAgent";
8+
import { prisma } from "@/lib/prisma";
9+
10+
11+
12+
13+
14+
export async function updateOrganization(organizationId: string, data: any) {
15+
try {
16+
const updatedOrg = await prisma.organization.update({
17+
where: { id: organizationId },
18+
data,
19+
})
20+
revalidatePath(`/organizations/${updatedOrg.url}`)
21+
return updatedOrg
22+
} catch (error) {
23+
console.error("Error updating organization:", error)
24+
throw new Error("Failed to update organization")
25+
}
26+
}
27+
28+
export async function getOrganization(organizationUrl: string, userId: string) {
29+
return await getOrCreateOrganizationFromUrl(organizationUrl, userId)
30+
}

app/organizations/page.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Suspense } from 'react'
2+
import { prisma } from '@/lib/prisma'
3+
import Link from 'next/link'
4+
import { Button } from '@/components/ui/button'
5+
import { Input } from '@/components/ui/input'
6+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
7+
import CreateOrganizationForm from './CreateOrganizationForm'
8+
import { Organization } from '@prisma/client'
9+
import { getServerSession } from 'next-auth/next'
10+
import { authOptions } from "@/lib/auth"
11+
12+
async function getOrganizations(search: string = '') {
13+
return prisma.organization.findMany({
14+
where: {
15+
OR: [
16+
{name: {contains: search, mode: 'insensitive'}},
17+
{description: {contains: search, mode: 'insensitive'}},
18+
],
19+
},
20+
orderBy: {name: 'asc'},
21+
});
22+
}
23+
24+
async function OrganizationsList({ search }: { search: string }) {
25+
const organizations = await getOrganizations(search)
26+
27+
return (
28+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
29+
{organizations.map((org: Organization) => (
30+
<Card key={org.id}>
31+
<CardHeader>
32+
<CardTitle>{org.name}</CardTitle>
33+
</CardHeader>
34+
<CardContent>
35+
<p className="text-sm text-gray-600 mb-4">{org.description}</p>
36+
<Link href={`/organizations/${org.slug}`}>
37+
<Button>View Details</Button>
38+
</Link>
39+
</CardContent>
40+
</Card>
41+
))}
42+
</div>
43+
)
44+
}
45+
46+
export default async function OrganizationsPage({
47+
searchParams,
48+
}: {
49+
searchParams: { search: string }
50+
}) {
51+
const search = searchParams.search || ''
52+
const session = await getServerSession(authOptions)
53+
const userId = session?.user?.id || ''
54+
55+
return (
56+
<div className="container mx-auto py-8">
57+
<h1 className="text-3xl font-bold mb-8">Organizations</h1>
58+
59+
<div className="mb-8">
60+
<form>
61+
<Input
62+
type="text"
63+
name="search"
64+
placeholder="Search organizations..."
65+
defaultValue={search}
66+
className="w-full max-w-md"
67+
/>
68+
</form>
69+
</div>
70+
71+
<Suspense fallback={<div>Loading organizations...</div>}>
72+
<OrganizationsList search={search} />
73+
</Suspense>
74+
75+
<div className="mt-12">
76+
<h2 className="text-2xl font-semibold mb-4">Create New Organization</h2>
77+
<CreateOrganizationForm userId={userId} />
78+
</div>
79+
</div>
80+
)
81+
}

0 commit comments

Comments
 (0)