feedback feature
This commit is contained in:
@@ -135,30 +135,35 @@ export default function CommunityHub() {
|
||||
|
||||
<main className="container mx-auto px-6 py-8 space-y-12">
|
||||
{/* User's Projects Section */}
|
||||
{isAuthenticated && (userProjects.length > 0 || localProjects.length > 0) && (
|
||||
{isAuthenticated && (
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-semibold">My Projects</h2>
|
||||
<CreateProjectButton onCreateProject={handleCreateProject} />
|
||||
</div>
|
||||
<ProjectGrid
|
||||
projects={[...userProjects, ...localProjects]}
|
||||
onProjectClick={handleProjectClick}
|
||||
onViewClick={handleViewProject}
|
||||
onSaveToCloud={handleSaveLocalToCloud}
|
||||
showOwner={false}
|
||||
canEdit
|
||||
onUpdate={() => {
|
||||
// Reload projects after settings update
|
||||
if (!authLoading) {
|
||||
getUserProjects().then((result) => {
|
||||
if (result.success) {
|
||||
setUserProjects(result.data || [])
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{userProjects.length === 0 && localProjects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
|
||||
<p className="text-muted-foreground">You don't have any projects yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectGrid
|
||||
projects={[...userProjects, ...localProjects]}
|
||||
onProjectClick={handleProjectClick}
|
||||
onViewClick={handleViewProject}
|
||||
onSaveToCloud={handleSaveLocalToCloud}
|
||||
showOwner={false}
|
||||
canEdit
|
||||
onUpdate={() => {
|
||||
if (!authLoading) {
|
||||
getUserProjects().then((result) => {
|
||||
if (result.success) {
|
||||
setUserProjects(result.data || [])
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useState } from 'react'
|
||||
import { createProject } from '../lib/projects/actions'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
|
||||
import { Switch } from '@/components/ui/primitives/switch'
|
||||
import { GoogleAddressSearch } from './google-address-search'
|
||||
|
||||
interface NewProjectDialogProps {
|
||||
open: boolean
|
||||
@@ -18,38 +17,20 @@ interface NewProjectDialogProps {
|
||||
}
|
||||
}
|
||||
|
||||
interface AddressData {
|
||||
streetNumber?: string
|
||||
route?: string
|
||||
city?: string
|
||||
state?: string
|
||||
postalCode?: string
|
||||
country?: string
|
||||
center: [number, number]
|
||||
formattedAddress: string
|
||||
}
|
||||
|
||||
/**
|
||||
* NewProjectDialog - Dialog for creating a new project with optional Google Maps address search
|
||||
*/
|
||||
export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectData }: NewProjectDialogProps) {
|
||||
const [projectName, setProjectName] = useState(localProjectData?.name || '')
|
||||
const [address, setAddress] = useState<AddressData | null>(null)
|
||||
const [showAddressSearch, setShowAddressSearch] = useState(false)
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleAddressSelect = (addressData: AddressData) => {
|
||||
setAddress(addressData)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
|
||||
const name = projectName.trim() || (address?.formattedAddress ?? 'Untitled Project')
|
||||
const name = projectName.trim() || 'Untitled Project'
|
||||
|
||||
if (!name) {
|
||||
setError('Please enter a project name')
|
||||
@@ -61,13 +42,6 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
|
||||
try {
|
||||
const result = await createProject({
|
||||
name,
|
||||
center: address?.center,
|
||||
streetNumber: address?.streetNumber,
|
||||
route: address?.route,
|
||||
city: address?.city,
|
||||
state: address?.state,
|
||||
postalCode: address?.postalCode,
|
||||
country: address?.country || 'US',
|
||||
isPrivate,
|
||||
sceneGraph: localProjectData?.sceneGraph,
|
||||
})
|
||||
@@ -75,8 +49,6 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
|
||||
if (result.success && result.data) {
|
||||
onOpenChange(false)
|
||||
setProjectName('')
|
||||
setAddress(null)
|
||||
setShowAddressSearch(false)
|
||||
setIsPrivate(false)
|
||||
onSuccess?.(result.data.id)
|
||||
} else {
|
||||
@@ -93,8 +65,6 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
|
||||
if (!isCreating) {
|
||||
onOpenChange(false)
|
||||
setProjectName('')
|
||||
setAddress(null)
|
||||
setShowAddressSearch(false)
|
||||
setIsPrivate(false)
|
||||
setError(null)
|
||||
}
|
||||
@@ -136,44 +106,6 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Optional Address Section */}
|
||||
{!showAddressSearch ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddressSearch(true)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isCreating}
|
||||
>
|
||||
+ Add an address (optional)
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Address (optional)</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddressSearch(false)
|
||||
setAddress(null)
|
||||
}}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
disabled={isCreating}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<GoogleAddressSearch onAddressSelect={handleAddressSelect} disabled={isCreating} />
|
||||
|
||||
{/* Show selected address */}
|
||||
{address && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
|
||||
<p className="font-medium">Selected Address:</p>
|
||||
<p className="mt-1 text-muted-foreground">{address.formattedAddress}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Toggle */}
|
||||
<div className="flex items-center justify-between rounded-md border border-border p-3">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
'use server'
|
||||
|
||||
import { createId } from '@pascal-app/db'
|
||||
import { createServerSupabaseClient } from '../database/server'
|
||||
import { getSession } from '../auth/server'
|
||||
|
||||
export async function submitFeedback(
|
||||
message: string,
|
||||
): Promise<{ success: true } | { success: false; error: string }> {
|
||||
try {
|
||||
const trimmed = message.trim()
|
||||
if (!trimmed) return { success: false, error: 'Message cannot be empty' }
|
||||
|
||||
const session = await getSession()
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
const { error } = await supabase.from('feedback').insert({
|
||||
id: createId('feedback'),
|
||||
user_id: session?.user?.id ?? null,
|
||||
message: trimmed,
|
||||
})
|
||||
|
||||
if (error) return { success: false, error: error.message }
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to submit feedback',
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user