feat: Add expandable section for creating products with integrated product form
This commit is contained in:
@@ -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.`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,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} />
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user