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> { async importFromInput(input: string): Promise<QuickImportResult> {
input = input.trim(); input = input.trim();
console.log('[QuickImport] Mottog input:', input);
if (!input) { if (!input) {
throw new BadRequestException('Du måste ange en URL eller filsökväg'); 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 isUrl = this.isUrl(input);
const isPdf = this.isPdfPath(input); const isPdf = this.isPdfPath(input);
console.log('[QuickImport] isUrl:', isUrl, 'isPdf:', isPdf);
if (isUrl) { if (isUrl) {
// Försök detektera webbplats // Försök detektera webbplats
if (input.includes('ica.se')) { if (input.includes('ica.se')) {
console.log('[QuickImport] Detekterade ICA-länk, startar skrapning...');
return this.scrapeIcaRecipe(input); return this.scrapeIcaRecipe(input);
} else { } else {
console.log('[QuickImport] URL är inte från ICA.se');
throw new BadRequestException( throw new BadRequestException(
'Endast ICA-recept stöds för närvarande. Försök med en ICA-länk (ica.se)' 'Endast ICA-recept stöds för närvarande. Försök med en ICA-länk (ica.se)'
); );
} }
} else if (isPdf) { } else if (isPdf) {
console.log('[QuickImport] PDF-fil identifierad');
throw new BadRequestException( throw new BadRequestException(
'PDF-import är under utveckling. Använd snabbimport för ICA-recept eller skriv in receptet manuellt.' 'PDF-import är under utveckling. Använd snabbimport för ICA-recept eller skriv in receptet manuellt.'
); );
} else { } else {
console.log('[QuickImport] Input är inte URL eller PDF');
throw new BadRequestException( throw new BadRequestException(
'Ogültig input. Ange en gyltig URL (t.ex. ica.se/recept/...) eller filsökväg' '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> { private async scrapeIcaRecipe(url: string): Promise<QuickImportResult> {
try { try {
console.log('[QuickImport] Hämtar HTML från:', url);
// Hämta HTML från URL // Hämta HTML från URL
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
@@ -82,14 +91,18 @@ export class QuickImportService {
}, },
}); });
console.log('[QuickImport] HTTP status:', response.status);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
const html = await response.text(); const html = await response.text();
console.log('[QuickImport] HTML längd:', html.length, 'tecken');
// Extrahera receptinformation från HTML // Extrahera receptinformation från HTML
const recipe = this.parseIcaHtml(html); const recipe = this.parseIcaHtml(html);
console.log('[QuickImport] Parsad recept:', { name: recipe.name, ingredienser: recipe.ingredients.length });
if (!recipe.name) { if (!recipe.name) {
throw new Error('Kunde inte hitta receptnamn på sidan. Försök med en annan länk.'); 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 // Konvertera till Markdown-format
const markdown = this.recipeToMarkdown(recipe); const markdown = this.recipeToMarkdown(recipe);
console.log('[QuickImport] Markdown genererad, längd:', markdown.length);
return { return {
markdown, markdown,
@@ -104,6 +118,7 @@ export class QuickImportService {
}; };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Okänt fel vid scraping'; const message = err instanceof Error ? err.message : 'Okänt fel vid scraping';
console.error('[QuickImport] ERROR:', message);
throw new BadRequestException( throw new BadRequestException(
`Kunde inte hämta recept från ICA: ${message}. Kontrollera att länken är korrekt och försök igen.` `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 Admin
</Link> </Link>
<Link <Link
href="/recipes/import" href="/recipes/create"
style={{ style={{
padding: '0.5rem 0.75rem', padding: '0.5rem 0.75rem',
background: '#fff', background: '#fff',
@@ -87,7 +87,7 @@ export default function Navigation() {
fontWeight: 500, fontWeight: 500,
}} }}
> >
📥 Importera recept Snabbimport recept
</Link> </Link>
</nav> </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 [successMessage, setSuccessMessage] = useState<string | null>(null);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isConfirming, setIsConfirming] = useState(false); const [isConfirming, setIsConfirming] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const fetchPreview = () => { const fetchPreview = () => {
setError(null); setError(null);
@@ -89,15 +90,44 @@ export default function MergePreviewForm({ products }: Props) {
return ( return (
<section <section
style={{ style={{
border: '1px solid #ddd', border: '2px solid #10b981',
borderRadius: '8px', borderRadius: '8px',
padding: '1rem',
marginBottom: '1.5rem', marginBottom: '1.5rem',
display: 'grid', overflow: 'hidden',
gap: '1rem',
}} }}
> >
<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))' }}> <div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
<label style={{ display: 'grid', gap: '0.3rem' }}> <label style={{ display: 'grid', gap: '0.3rem' }}>
@@ -310,7 +340,7 @@ export default function MergePreviewForm({ products }: Props) {
</article> </article>
) : null} ) : null}
</div> </div>
) : null} )}
</section> </section>
); );
} }
+3
View File
@@ -3,6 +3,7 @@ import type { Product } from '../../../features/inventory/types';
import MergePreviewForm from './MergePreviewForm'; import MergePreviewForm from './MergePreviewForm';
import AdminProductList from './AdminProductList'; import AdminProductList from './AdminProductList';
import Navigation from '../../Navigation'; import Navigation from '../../Navigation';
import ExpandableCreateProductSection from './ExpandableCreateProductSection';
export default async function AdminProductsPage() { export default async function AdminProductsPage() {
const products = await fetchJson<Product[]>('/api/products'); const products = await fetchJson<Product[]>('/api/products');
@@ -13,6 +14,8 @@ export default async function AdminProductsPage() {
<h1 style={{ marginBottom: '1.5rem' }}>Admin: Produkter</h1> <h1 style={{ marginBottom: '1.5rem' }}>Admin: Produkter</h1>
<p>Här kan du granska och standardisera produktnamn.</p> <p>Här kan du granska och standardisera produktnamn.</p>
<ExpandableCreateProductSection />
<MergePreviewForm products={products} /> <MergePreviewForm products={products} />
<AdminProductList products={products} /> <AdminProductList products={products} />
-2
View File
@@ -1,5 +1,4 @@
import InventoryForm from './InventoryForm'; import InventoryForm from './InventoryForm';
import ProductForm from './ProductForm';
import Link from 'next/link'; import Link from 'next/link';
import { fetchJson } from '../../lib/api'; import { fetchJson } from '../../lib/api';
import type { InventoryItem, Product } from '../../features/inventory/types'; import type { InventoryItem, Product } from '../../features/inventory/types';
@@ -109,7 +108,6 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps
<Navigation /> <Navigation />
<h1 style={{ marginBottom: '1.5rem' }}>Varor hemma</h1> <h1 style={{ marginBottom: '1.5rem' }}>Varor hemma</h1>
<ProductForm />
<InventoryForm products={products} /> <InventoryForm products={products} />
<section style={{ marginBottom: '1.5rem' }}> <section style={{ marginBottom: '1.5rem' }}>