Bilder + GSC + Posthog
This commit is contained in:
@@ -4,58 +4,68 @@ import path from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR ?? (process.env.NODE_ENV === 'production' ? '/app/uploads' : './uploads')
|
||||
const MAX_SIZE_BYTES = Number(process.env.UPLOAD_MAX_SIZE_MB ?? 10) * 1024 * 1024
|
||||
|
||||
function getUploadRoot() {
|
||||
if (path.isAbsolute(UPLOAD_DIR)) {
|
||||
return UPLOAD_DIR
|
||||
}
|
||||
return path.resolve(process.cwd(), UPLOAD_DIR)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// Auth check
|
||||
const session = await auth.api.getSession({ headers: await getSanitizedHeaders(req.headers) })
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE_BYTES) {
|
||||
return NextResponse.json({ error: 'File too large' }, { status: 413 })
|
||||
}
|
||||
|
||||
// Only allow safe file types
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
]
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'File type not allowed' }, { status: 415 })
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name)
|
||||
const fileName = `${randomUUID()}${ext}`
|
||||
const uploadPath = getUploadRoot()
|
||||
|
||||
await mkdir(uploadPath, { recursive: true })
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await writeFile(path.join(uploadPath, fileName), buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
storagePath: fileName,
|
||||
name: file.name,
|
||||
sizeBytes: file.size,
|
||||
url: `/uploads/${fileName}`,
|
||||
})
|
||||
}
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR ?? (process.env.NODE_ENV === 'production' ? '/app/uploads' : './uploads')
|
||||
const MAX_SIZE_BYTES = Number(process.env.UPLOAD_MAX_SIZE_MB ?? 10) * 1024 * 1024
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
function getUploadRoot() {
|
||||
if (path.isAbsolute(UPLOAD_DIR)) {
|
||||
return UPLOAD_DIR
|
||||
}
|
||||
return path.resolve(process.cwd(), UPLOAD_DIR)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Auth check
|
||||
const session = await auth.api.getSession({ headers: await getSanitizedHeaders(req.headers) })
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE_BYTES) {
|
||||
return NextResponse.json({ error: 'File too large' }, { status: 413 })
|
||||
}
|
||||
|
||||
// Only allow safe file types
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
]
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'File type not allowed' }, { status: 415 })
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name)
|
||||
const fileName = `${randomUUID()}${ext}`
|
||||
const uploadPath = getUploadRoot()
|
||||
|
||||
await mkdir(uploadPath, { recursive: true })
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await writeFile(path.join(uploadPath, fileName), buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
storagePath: fileName,
|
||||
name: file.name,
|
||||
sizeBytes: file.size,
|
||||
url: `/uploads/${fileName}`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Upload failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { Syne } from 'next/font/google'
|
||||
import { ArrowRight, ArrowUpRight, Sun, Moon, Menu, X } from 'lucide-react'
|
||||
import posthog from 'posthog-js'
|
||||
import { initPosthog } from '../instrumentation-client'
|
||||
|
||||
const syne = Syne({ subsets: ['latin'], weight: ['400', '500', '600', '700', '800'] })
|
||||
|
||||
@@ -75,21 +77,17 @@ export default function RootPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (cookieConsent === 'accepted') {
|
||||
window.posthog?.opt_in_capturing?.()
|
||||
const sendPageView = () => {
|
||||
window.posthog?.capture?.('landing_page_viewed', {
|
||||
path: window.location.pathname,
|
||||
})
|
||||
}
|
||||
if (window.posthog?.capture) {
|
||||
sendPageView()
|
||||
} else {
|
||||
window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT))
|
||||
window.setTimeout(sendPageView, 150)
|
||||
}
|
||||
window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT))
|
||||
initPosthog()
|
||||
posthog.opt_in_capturing()
|
||||
posthog.capture('$pageview', {
|
||||
$current_url: window.location.href,
|
||||
path: window.location.pathname,
|
||||
referrer: document.referrer,
|
||||
})
|
||||
}
|
||||
if (cookieConsent === 'declined') {
|
||||
window.posthog?.opt_out_capturing?.()
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
}, [cookieConsent])
|
||||
|
||||
@@ -110,18 +108,12 @@ export default function RootPage() {
|
||||
|
||||
const trackLandingCta = (placement: string) => {
|
||||
if (cookieConsent !== 'accepted') return
|
||||
const sendCta = () => {
|
||||
window.posthog?.capture?.('landing_cta_clicked', {
|
||||
placement,
|
||||
target: 'contact_email',
|
||||
})
|
||||
}
|
||||
if (window.posthog?.capture) {
|
||||
sendCta()
|
||||
} else {
|
||||
window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT))
|
||||
window.setTimeout(sendCta, 150)
|
||||
}
|
||||
window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT))
|
||||
initPosthog()
|
||||
posthog.capture('landing_cta_clicked', {
|
||||
placement,
|
||||
target: 'contact_email',
|
||||
})
|
||||
}
|
||||
|
||||
const handleContactCtaClick = (placement: string) => {
|
||||
|
||||
@@ -5,9 +5,20 @@ import { useRouter } from 'next/navigation'
|
||||
import { createOrganization } from './actions'
|
||||
import { LandingPagePreview } from './LandingPagePreview'
|
||||
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
export function CreateOrgForm() {
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
async function readUploadResponse(res: Response) {
|
||||
const data = await res.json().catch(() => null)
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.error || 'Upload fehlgeschlagen')
|
||||
}
|
||||
if (!data?.url) {
|
||||
throw new Error('Upload-Antwort enthaelt keine Datei-URL')
|
||||
}
|
||||
return data as { url: string }
|
||||
}
|
||||
|
||||
export function CreateOrgForm() {
|
||||
const [state, formAction, isPending] = useActionState(createOrganization, initialState)
|
||||
const router = useRouter()
|
||||
const [step, setStep] = useState(1)
|
||||
@@ -69,20 +80,19 @@ export function CreateOrgForm() {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
setFormData(prev => ({ ...prev, logoUrl: data.url }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await readUploadResponse(res)
|
||||
setFormData(prev => ({ ...prev, logoUrl: data.url }))
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
alert(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const [isHeroUploading, setIsHeroUploading] = useState(false)
|
||||
@@ -98,20 +108,19 @@ export function CreateOrgForm() {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
setFormData(prev => ({ ...prev, landingPageHeroImage: data.url }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
} finally {
|
||||
setIsHeroUploading(false)
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await readUploadResponse(res)
|
||||
setFormData(prev => ({ ...prev, landingPageHeroImage: data.url }))
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
alert(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
|
||||
} finally {
|
||||
setIsHeroUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server'
|
||||
|
||||
import { prisma, Prisma } from '@innungsapp/shared'
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
@@ -13,13 +13,13 @@ function normalizeEmail(email: string | null | undefined): string {
|
||||
return (email ?? '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function toJsonbText(value: string | undefined): Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput {
|
||||
if (!value) {
|
||||
return Prisma.DbNull
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
function toJsonbText(value: string | undefined): string | null {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a credential (email+password) account for a user.
|
||||
|
||||
@@ -43,9 +43,20 @@ interface Props {
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
export function EditOrgForm({ org }: Props) {
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
async function readUploadResponse(res: Response) {
|
||||
const data = await res.json().catch(() => null)
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.error || 'Upload fehlgeschlagen')
|
||||
}
|
||||
if (!data?.url) {
|
||||
throw new Error('Upload-Antwort enthaelt keine Datei-URL')
|
||||
}
|
||||
return data as { url: string }
|
||||
}
|
||||
|
||||
export function EditOrgForm({ org }: Props) {
|
||||
const boundAction = updateOrganization.bind(null, org.id)
|
||||
const [state, formAction, isPending] = useActionState(boundAction, initialState)
|
||||
const [logoUrl, setLogoUrl] = useState(org.logoUrl || '')
|
||||
@@ -65,21 +76,20 @@ export function EditOrgForm({ org }: Props) {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
if (type === 'logo') setLogoUrl(data.url)
|
||||
if (type === 'hero') setHeroImageUrl(data.url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
} finally {
|
||||
setIsUploading(prev => ({ ...prev, [type]: false }))
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: uploadFormData
|
||||
})
|
||||
const data = await readUploadResponse(res)
|
||||
if (type === 'logo') setLogoUrl(data.url)
|
||||
if (type === 'hero') setHeroImageUrl(data.url)
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
alert(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
|
||||
} finally {
|
||||
setIsUploading(prev => ({ ...prev, [type]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user