feat: Add expandable section for creating products with integrated product form

This commit is contained in:
Nils-Johan Gynther
2026-04-12 08:29:28 +02:00
parent f84ee39197
commit ea307e6a6c
6 changed files with 110 additions and 10 deletions
@@ -12,6 +12,7 @@ export class QuickImportService {
*/
async importFromInput(input: string): Promise<QuickImportResult> {
input = input.trim();
console.log('[QuickImport] Mottog input:', input);
if (!input) {
throw new BadRequestException('Du måste ange en URL eller filsökväg');
@@ -21,20 +22,26 @@ export class QuickImportService {
const isUrl = this.isUrl(input);
const isPdf = this.isPdfPath(input);
console.log('[QuickImport] isUrl:', isUrl, 'isPdf:', isPdf);
if (isUrl) {
// Försök detektera webbplats
if (input.includes('ica.se')) {
console.log('[QuickImport] Detekterade ICA-länk, startar skrapning...');
return this.scrapeIcaRecipe(input);
} else {
console.log('[QuickImport] URL är inte från ICA.se');
throw new BadRequestException(
'Endast ICA-recept stöds för närvarande. Försök med en ICA-länk (ica.se)'
);
}
} else if (isPdf) {
console.log('[QuickImport] PDF-fil identifierad');
throw new BadRequestException(
'PDF-import är under utveckling. Använd snabbimport för ICA-recept eller skriv in receptet manuellt.'
);
} else {
console.log('[QuickImport] Input är inte URL eller PDF');
throw new BadRequestException(
'Ogültig input. Ange en gyltig URL (t.ex. ica.se/recept/...) eller filsökväg'
);
@@ -74,6 +81,8 @@ export class QuickImportService {
*/
private async scrapeIcaRecipe(url: string): Promise<QuickImportResult> {
try {
console.log('[QuickImport] Hämtar HTML från:', url);
// Hämta HTML från URL
const response = await fetch(url, {
headers: {
@@ -82,14 +91,18 @@ export class QuickImportService {
},
});
console.log('[QuickImport] HTTP status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
console.log('[QuickImport] HTML längd:', html.length, 'tecken');
// Extrahera receptinformation från HTML
const recipe = this.parseIcaHtml(html);
console.log('[QuickImport] Parsad recept:', { name: recipe.name, ingredienser: recipe.ingredients.length });
if (!recipe.name) {
throw new Error('Kunde inte hitta receptnamn på sidan. Försök med en annan länk.');
@@ -97,6 +110,7 @@ export class QuickImportService {
// Konvertera till Markdown-format
const markdown = this.recipeToMarkdown(recipe);
console.log('[QuickImport] Markdown genererad, längd:', markdown.length);
return {
markdown,
@@ -104,6 +118,7 @@ export class QuickImportService {
};
} catch (err) {
const message = err instanceof Error ? err.message : 'Okänt fel vid scraping';
console.error('[QuickImport] ERROR:', message);
throw new BadRequestException(
`Kunde inte hämta recept från ICA: ${message}. Kontrollera att länken är korrekt och försök igen.`
);
+2 -2
View File
@@ -75,7 +75,7 @@ export default function Navigation() {
Admin
</Link>
<Link
href="/recipes/import"
href="/recipes/create"
style={{
padding: '0.5rem 0.75rem',
background: '#fff',
@@ -87,7 +87,7 @@ export default function Navigation() {
fontWeight: 500,
}}
>
📥 Importera recept
Snabbimport recept
</Link>
</nav>
);
@@ -0,0 +1,54 @@
'use client';
import { useState } from 'react';
import ProductForm from '../../inventory/ProductForm';
export default function ExpandableCreateProductSection() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<section
style={{
border: '2px solid #0070f3',
borderRadius: '8px',
marginBottom: '1.5rem',
overflow: 'hidden',
}}
>
<button
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: '100%',
padding: '1rem',
background: '#0070f3',
color: 'white',
border: 'none',
fontSize: '1.1rem',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
transition: 'background 0.2s',
}}
onMouseEnter={(e) => {
(e.target as HTMLElement).style.background = '#0059cc';
}}
onMouseLeave={(e) => {
(e.target as HTMLElement).style.background = '#0070f3';
}}
>
<span> Skapa produkt</span>
<span style={{ fontSize: '1.5rem', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
</span>
</button>
{isExpanded && (
<div style={{ padding: '1rem', background: '#f9f9f9' }}>
<ProductForm />
</div>
)}
</section>
);
}
@@ -16,6 +16,7 @@ export default function MergePreviewForm({ products }: Props) {
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const [isConfirming, setIsConfirming] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const fetchPreview = () => {
setError(null);
@@ -89,15 +90,44 @@ export default function MergePreviewForm({ products }: Props) {
return (
<section
style={{
border: '1px solid #ddd',
border: '2px solid #10b981',
borderRadius: '8px',
padding: '1rem',
marginBottom: '1.5rem',
display: 'grid',
gap: '1rem',
overflow: 'hidden',
}}
>
<h2 style={{ margin: 0 }}>Förhandsgranska merge</h2>
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: '100%',
padding: '1rem',
background: '#10b981',
color: 'white',
border: 'none',
fontSize: '1.1rem',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
transition: 'background 0.2s',
}}
onMouseEnter={(e) => {
(e.target as HTMLElement).style.background = '#059669';
}}
onMouseLeave={(e) => {
(e.target as HTMLElement).style.background = '#10b981';
}}
>
<span>🔄 Förhandsgranska merge</span>
<span style={{ fontSize: '1.5rem', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
</span>
</button>
{isExpanded && (
<div style={{ padding: '1rem', background: '#f9fafb', display: 'grid', gap: '1rem' }}
<div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
<label style={{ display: 'grid', gap: '0.3rem' }}>
@@ -310,7 +340,7 @@ export default function MergePreviewForm({ products }: Props) {
</article>
) : null}
</div>
) : null}
)}
</section>
);
}
+3
View File
@@ -3,6 +3,7 @@ import type { Product } from '../../../features/inventory/types';
import MergePreviewForm from './MergePreviewForm';
import AdminProductList from './AdminProductList';
import Navigation from '../../Navigation';
import ExpandableCreateProductSection from './ExpandableCreateProductSection';
export default async function AdminProductsPage() {
const products = await fetchJson<Product[]>('/api/products');
@@ -13,6 +14,8 @@ export default async function AdminProductsPage() {
<h1 style={{ marginBottom: '1.5rem' }}>Admin: Produkter</h1>
<p>Här kan du granska och standardisera produktnamn.</p>
<ExpandableCreateProductSection />
<MergePreviewForm products={products} />
<AdminProductList products={products} />
-2
View File
@@ -1,5 +1,4 @@
import InventoryForm from './InventoryForm';
import ProductForm from './ProductForm';
import Link from 'next/link';
import { fetchJson } from '../../lib/api';
import type { InventoryItem, Product } from '../../features/inventory/types';
@@ -109,7 +108,6 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps
<Navigation />
<h1 style={{ marginBottom: '1.5rem' }}>Varor hemma</h1>
<ProductForm />
<InventoryForm products={products} />
<section style={{ marginBottom: '1.5rem' }}>