feat: add TypeScript definitions for next-auth session with accessToken and user details
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import ReceiptImportClient from '../kvitto/ReceiptImportClient';
|
||||
import { parseErrorResponse } from '../../lib/error-handler';
|
||||
|
||||
type Tab = 'kvitto' | 'recept';
|
||||
|
||||
type Product = { id: number; name: string; canonicalName: string | null };
|
||||
|
||||
export default function ImportTabsClient({ activeTab, isAdmin }: { activeTab: Tab; isAdmin: boolean }) {
|
||||
return (
|
||||
<main style={{ padding: '1rem', maxWidth: '900px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '1rem' }}>Importera</h1>
|
||||
|
||||
{/* Flikar */}
|
||||
<div style={{ display: 'flex', gap: '0', marginBottom: '1.5rem', borderBottom: '2px solid #e5e7eb' }}>
|
||||
<Link
|
||||
href="/import?tab=kvitto"
|
||||
style={{
|
||||
padding: '0.6rem 1.25rem',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.95rem',
|
||||
textDecoration: 'none',
|
||||
borderBottom: activeTab === 'kvitto' ? '2px solid #0070f3' : '2px solid transparent',
|
||||
marginBottom: '-2px',
|
||||
color: activeTab === 'kvitto' ? '#0070f3' : '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
🧾 Kvitto
|
||||
</Link>
|
||||
<Link
|
||||
href="/import?tab=recept"
|
||||
style={{
|
||||
padding: '0.6rem 1.25rem',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.95rem',
|
||||
textDecoration: 'none',
|
||||
borderBottom: activeTab === 'recept' ? '2px solid #0070f3' : '2px solid transparent',
|
||||
marginBottom: '-2px',
|
||||
color: activeTab === 'recept' ? '#0070f3' : '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
📋 Recept
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Innehåll */}
|
||||
{activeTab === 'kvitto' && (
|
||||
<div>
|
||||
<p style={{ color: '#666', marginBottom: '1.5rem' }}>
|
||||
Fotografera eller ladda upp ett kvitto — varorna läggs till i ditt inventarie.
|
||||
</p>
|
||||
<ReceiptImportClient isAdmin={isAdmin} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'recept' && <ReceptImport />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function ReceptImport() {
|
||||
const router = useRouter();
|
||||
const [selectedMethod, setSelectedMethod] = useState<'file' | 'url'>('file');
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [url, setUrl] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFileSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!selectedFile) { setError('Välj en PDF eller bildfil först.'); return; }
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
const res = await fetch('/api/quick-import-proxy', { method: 'POST', body: formData });
|
||||
if (!res.ok) throw new Error(await parseErrorResponse(res));
|
||||
const data = await res.json();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[ImportTabsClient:file] quick-import response', {
|
||||
imageUrl: data.imageUrl ?? null,
|
||||
imageWarning: data.imageWarning ?? null,
|
||||
markdownLength: (data.markdown ?? '').length,
|
||||
});
|
||||
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
|
||||
if (data.imageUrl) {
|
||||
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
|
||||
} else {
|
||||
sessionStorage.removeItem('prefilled_image_url');
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[ImportTabsClient:file] sessionStorage snapshot', {
|
||||
prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
|
||||
prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
|
||||
});
|
||||
router.push('/recipes/write');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!url.trim()) { setError('Vänligen ange en URL.'); return; }
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/quick-import-proxy', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ input: url.trim() }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await parseErrorResponse(res));
|
||||
const data = await res.json();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[ImportTabsClient:url] quick-import response', {
|
||||
imageUrl: data.imageUrl ?? null,
|
||||
imageWarning: data.imageWarning ?? null,
|
||||
markdownLength: (data.markdown ?? '').length,
|
||||
});
|
||||
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
|
||||
if (data.imageUrl) {
|
||||
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
|
||||
} else {
|
||||
sessionStorage.removeItem('prefilled_image_url');
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[ImportTabsClient:url] sessionStorage snapshot', {
|
||||
prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
|
||||
prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
|
||||
});
|
||||
router.push('/recipes/write');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p style={{ color: '#666', marginBottom: '1.5rem' }}>
|
||||
Ladda upp en PDF eller bild för OCR, eller ange en receptlänk.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div style={{ background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: '6px', padding: '1rem', marginBottom: '1.5rem', color: '#dc2626', fontSize: '0.95rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Välj metod */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
||||
{(['file', 'url'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setSelectedMethod(m)}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
border: '1px solid',
|
||||
borderColor: selectedMethod === m ? '#0070f3' : '#d1d5db',
|
||||
borderRadius: '6px',
|
||||
background: selectedMethod === m ? '#eff6ff' : '#fff',
|
||||
color: selectedMethod === m ? '#0070f3' : '#555',
|
||||
fontWeight: selectedMethod === m ? 600 : 400,
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{m === 'file' ? '📄 Fil / PDF' : '🔗 Länk'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedMethod === 'file' && (
|
||||
<form onSubmit={handleFileSubmit} style={{ display: 'grid', gap: '0.75rem', maxWidth: '500px' }}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.png,.jpg,.jpeg,.webp,.bmp"
|
||||
onChange={(e) => setSelectedFile(e.target.files?.[0] ?? null)}
|
||||
style={{ padding: '0.75rem', background: 'white', border: '1px solid #cbd5e1', borderRadius: '6px' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!selectedFile || isLoading}
|
||||
style={{ padding: '0.75rem', background: '#0070f3', color: 'white', border: 'none', borderRadius: '4px', cursor: !selectedFile || isLoading ? 'not-allowed' : 'pointer', opacity: !selectedFile || isLoading ? 0.6 : 1, fontWeight: 600 }}
|
||||
>
|
||||
{isLoading ? 'Importerar...' : 'Importera fil'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{selectedMethod === 'url' && (
|
||||
<form onSubmit={handleUrlSubmit} style={{ display: 'grid', gap: '0.75rem', maxWidth: '500px' }}>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://exempel.se/recept/..."
|
||||
style={{ padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '0.9rem' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!url.trim() || isLoading}
|
||||
style={{ padding: '0.75rem', background: '#10b981', color: 'white', border: 'none', borderRadius: '4px', cursor: !url.trim() || isLoading ? 'not-allowed' : 'pointer', opacity: !url.trim() || isLoading ? 0.6 : 1, fontWeight: 600 }}
|
||||
>
|
||||
{isLoading ? 'Importerar...' : 'Importera från länk'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div style={{ background: '#f0fdf4', border: '1px solid #86efac', borderRadius: '6px', padding: '1rem', marginTop: '1.5rem', color: '#166534', fontSize: '0.9rem' }}>
|
||||
Efter import öppnas receptet automatiskt i redigeringsläget.
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
|
||||
<Link href="/recipes/write" style={{ padding: '0.75rem 1.5rem', background: 'transparent', border: '1px solid #ddd', borderRadius: '4px', textDecoration: 'none', color: '#333', fontWeight: 500 }}>
|
||||
Skriv in recept istället
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Metadata } from 'next';
|
||||
import Navigation from '../Navigation';
|
||||
import ImportTabsClient from './ImportTabsClient';
|
||||
import { auth } from '../../auth';
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ tab?: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
|
||||
const { tab } = await searchParams;
|
||||
if (tab === 'recept') return { title: 'Importera recept' };
|
||||
return { title: 'Importera kvitto' };
|
||||
}
|
||||
|
||||
export default async function ImportPage({ searchParams }: Props) {
|
||||
const { tab } = await searchParams;
|
||||
const activeTab = tab === 'recept' ? 'recept' : 'kvitto';
|
||||
const session = await auth();
|
||||
const isAdmin = (session?.user as any)?.role === 'admin';
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<ImportTabsClient activeTab={activeTab} isAdmin={isAdmin} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user