\ No newline at end of file diff --git a/admin/product-delete.php b/admin/product-delete.php new file mode 100644 index 0000000..f5ea93d --- /dev/null +++ b/admin/product-delete.php @@ -0,0 +1,31 @@ +prepare("DELETE FROM products WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + // Redireciona de volta para a lista de produtos com uma mensagem de sucesso + header("Location: products.php?status=deleted"); + exit; + + } catch (PDOException $e) { + // Em caso de erro, redireciona com uma mensagem de erro + // Em um ambiente real, você deveria logar o erro + header("Location: products.php?status=error"); + exit; + } +} else { + // Se o ID não for válido, apenas redireciona de volta + header("Location: products.php"); + exit; +} +?> \ No newline at end of file diff --git a/admin/product-edit.php b/admin/product-edit.php new file mode 100644 index 0000000..5c78033 --- /dev/null +++ b/admin/product-edit.php @@ -0,0 +1,136 @@ + '', + 'name' => '', + 'description' => '', + 'price' => '', + 'sku' => '', + 'stock' => '', + 'images' => '' +]; +$page_title = 'Adicionar Novo Produto'; +$image_path = ''; + +// Check if an ID is provided for editing +if (isset($_GET['id'])) { + $product_id = $_GET['id']; + $stmt = db()->prepare("SELECT * FROM products WHERE id = ?"); + $stmt->execute([$product_id]); + $product = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($product) { + $page_title = 'Editar Produto'; + $image_path = $product['images']; + } else { + header("Location: products.php"); + exit; + } +} + +// Handle form submission +if ($_SERVER["REQUEST_METHOD"] == "POST") { + $id = $_POST['id'] ?? null; + $name = $_POST['name']; + $description = $_POST['description']; + $price = $_POST['price']; + $sku = $_POST['sku']; + $stock = $_POST['stock']; + $current_image = $_POST['current_image'] ?? ''; + $image_path = $current_image; + + // Handle file upload + if (isset($_FILES['image']) && $_FILES['image']['error'] == 0) { + $upload_dir = '../assets/images/products/'; + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0775, true); + } + + $file_extension = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION); + $unique_filename = uniqid('product_', true) . '.' . $file_extension; + $target_file = $upload_dir . $unique_filename; + + if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) { + $image_path = 'assets/images/products/' . $unique_filename; + // Delete old image if it exists + if ($current_image && file_exists('../' . $current_image)) { + unlink('../' . $current_image); + } + } + } + + if ($id) { + // Update existing product + $stmt = db()->prepare("UPDATE products SET name = ?, description = ?, price = ?, sku = ?, stock = ?, images = ? WHERE id = ?"); + $stmt->execute([$name, $description, $price, $sku, $stock, $image_path, $id]); + } else { + // Insert new product + $stmt = db()->prepare("INSERT INTO products (name, description, price, sku, stock, images) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$name, $description, $price, $sku, $stock, $image_path]); + } + + header("Location: products.php?status=saved"); + exit; +} +?> + +
+

+ +
+
+
+ + + +
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ Imagem do Produto +
+ + + Envie uma nova imagem para substituir a atual. Formatos aceitos: JPG, PNG, GIF. +
+ + + Cancelar +
+
+
+
+ + \ No newline at end of file diff --git a/admin/products.php b/admin/products.php new file mode 100644 index 0000000..3ee1067 --- /dev/null +++ b/admin/products.php @@ -0,0 +1,71 @@ +query('SELECT id, name, price, stock, images FROM products ORDER BY created_at DESC'); + $products = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + die("Erro ao buscar produtos: " . $e->getMessage()); +} +?> + + +
Produto excluído com sucesso!
+ +
Ocorreu um erro ao processar sua solicitação.
+ +
Produto salvo com sucesso!
+ + +
+

Gerenciar Produtos

+ Adicionar Novo Produto +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ImagemIDNomePreçoEstoqueAções
Nenhum produto cadastrado.
+ + <?php echo htmlspecialchars($product['name']); ?> + + Sem imagem + + R$ + Editar + Excluir +
+
+ + \ No newline at end of file diff --git a/admin/style.css b/admin/style.css new file mode 100644 index 0000000..c4d4871 --- /dev/null +++ b/admin/style.css @@ -0,0 +1,112 @@ +/* Admin Styles */ +body { + font-family: 'Lato', sans-serif; + background-color: #f8f9fa; + color: #212529; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.login-container { + width: 100%; + max-width: 400px; + padding: 2rem; +} + +.login-form { + background-color: #fff; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1); +} + +.login-form h1 { + font-family: 'Montserrat', sans-serif; + text-align: center; + margin-bottom: 1.5rem; + color: #004A7F; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 700; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +button { + width: 100%; + padding: 0.75rem; + background-color: #004A7F; + color: #fff; + border: none; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 700; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #003359; +} + +/* Admin Panel Layout */ +.admin-wrapper { + display: flex; + height: 100vh; + width: 100vw; + align-items: stretch; +} + +.sidebar { + width: 250px; + background-color: #004A7F; + color: #fff; + padding: 1rem; + flex-shrink: 0; +} + +.sidebar h2 { + font-family: 'Montserrat', sans-serif; + text-align: center; + color: #FDB813; +} + +.sidebar nav ul { + list-style: none; + padding: 0; +} + +.sidebar nav a { + display: block; + padding: 0.75rem 1rem; + color: #fff; + text-decoration: none; + border-radius: 0.25rem; + margin-bottom: 0.5rem; +} + +.sidebar nav a:hover, +.sidebar nav a.active { + background-color: #003359; +} + +.main-content { + flex-grow: 1; + padding: 2rem; + overflow-y: auto; +} diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php new file mode 100644 index 0000000..00b1b00 --- /dev/null +++ b/ai/LocalAIApi.php @@ -0,0 +1,311 @@ + [ +// ['role' => 'system', 'content' => 'You are a helpful assistant.'], +// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], +// ], +// ]); +// if (!empty($response['success'])) { +// $decoded = LocalAIApi::decodeJsonFromResponse($response); +// } + +class LocalAIApi +{ + /** @var array|null */ + private static ?array $configCache = null; + + /** + * Signature compatible with the OpenAI Responses API. + * + * @param array $params Request body (model, input, text, reasoning, metadata, etc.). + * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). + * @return array{ + * success:bool, + * status?:int, + * data?:mixed, + * error?:string, + * response?:mixed, + * message?:string + * } + */ + public static function createResponse(array $params, array $options = []): array + { + $cfg = self::config(); + $payload = $params; + + if (empty($payload['input']) || !is_array($payload['input'])) { + return [ + 'success' => false, + 'error' => 'input_missing', + 'message' => 'Parameter "input" is required and must be an array.', + ]; + } + + if (!isset($payload['model']) || $payload['model'] === '') { + $payload['model'] = $cfg['default_model']; + } + + return self::request($options['path'] ?? null, $payload, $options); + } + + /** + * Snake_case alias for createResponse (matches the provided example). + * + * @param array $params + * @param array $options + * @return array + */ + public static function create_response(array $params, array $options = []): array + { + return self::createResponse($params, $options); + } + + /** + * Perform a raw request to the AI proxy. + * + * @param string $path Endpoint (may be an absolute URL). + * @param array $payload JSON payload. + * @param array $options Additional request options. + * @return array + */ + public static function request(?string $path = null, array $payload = [], array $options = []): array + { + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'curl_missing', + 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', + ]; + } + + $cfg = self::config(); + + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting AI request.', + ]; + } + + $defaultPath = $cfg['responses_path'] ?? null; + $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); + if (empty($resolvedPath)) { + return [ + 'success' => false, + 'error' => 'project_id_missing', + 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', + ]; + } + + $url = self::buildUrl($resolvedPath, $cfg['base_url']); + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + ]; + $headers[] = $projectHeader . ': ' . $projectUuid; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { + $payload['project_uuid'] = $projectUuid; + } + + $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if ($body === false) { + return [ + 'success' => false, + 'error' => 'json_encode_failed', + 'message' => 'Failed to encode request body to JSON.', + ]; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + + $responseBody = curl_exec($ch); + if ($responseBody === false) { + $error = curl_error($ch) ?: 'Unknown cURL error'; + curl_close($ch); + return [ + 'success' => false, + 'error' => 'curl_error', + 'message' => $error, + ]; + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $decoded = null; + if ($responseBody !== '' && $responseBody !== null) { + $decoded = json_decode($responseBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $decoded = null; + } + } + + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded ?? $responseBody, + ]; + } + + $errorMessage = 'AI proxy request failed'; + if (is_array($decoded)) { + $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; + } elseif (is_string($responseBody) && $responseBody !== '') { + $errorMessage = $responseBody; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $errorMessage, + 'response' => $decoded ?? $responseBody, + ]; + } + + /** + * Extract plain text from a Responses API payload. + * + * @param array $response Result of LocalAIApi::createResponse|request. + * @return string + */ + public static function extractText(array $response): string + { + $payload = $response['data'] ?? $response; + if (!is_array($payload)) { + return ''; + } + + if (!empty($payload['output']) && is_array($payload['output'])) { + $combined = ''; + foreach ($payload['output'] as $item) { + if (!isset($item['content']) || !is_array($item['content'])) { + continue; + } + foreach ($item['content'] as $block) { + if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { + $combined .= $block['text']; + } + } + } + if ($combined !== '') { + return $combined; + } + } + + if (!empty($payload['choices'][0]['message']['content'])) { + return (string) $payload['choices'][0]['message']['content']; + } + + return ''; + } + + /** + * Attempt to decode JSON emitted by the model (handles markdown fences). + * + * @param array $response + * @return array|null + */ + public static function decodeJsonFromResponse(array $response): ?array + { + $text = self::extractText($response); + if ($text === '') { + return null; + } + + $decoded = json_decode($text, true); + if (is_array($decoded)) { + return $decoded; + } + + $stripped = preg_replace('/^```json|```$/m', '', trim($text)); + if ($stripped !== null && $stripped !== $text) { + $decoded = json_decode($stripped, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } + + /** + * Load configuration from ai/config.php. + * + * @return array + */ + private static function config(): array + { + if (self::$configCache === null) { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new RuntimeException('AI config file not found: ai/config.php'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new RuntimeException('Invalid AI config format: expected array'); + } + self::$configCache = $cfg; + } + + return self::$configCache; + } + + /** + * Build an absolute URL from base_url and a path. + */ + private static function buildUrl(string $path, string $baseUrl): string + { + $trimmed = trim($path); + if ($trimmed === '') { + return $baseUrl; + } + if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { + return $trimmed; + } + if ($trimmed[0] === '/') { + return $baseUrl . $trimmed; + } + return $baseUrl . '/' . $trimmed; + } +} + +// Legacy alias for backward compatibility with the previous class name. +if (!class_exists('OpenAIService')) { + class_alias(LocalAIApi::class, 'OpenAIService'); +} diff --git a/ai/config.php b/ai/config.php new file mode 100644 index 0000000..1ba1596 --- /dev/null +++ b/ai/config.php @@ -0,0 +1,52 @@ + $baseUrl, + 'responses_path' => $responsesPath, + 'project_id' => $projectId, + 'project_uuid' => $projectUuid, + 'project_header' => 'project-uuid', + 'default_model' => 'gpt-5', + 'timeout' => 30, + 'verify_tls' => true, +]; diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..7689203 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,154 @@ +/* General Body Styles */ +body { + font-family: 'Lato', sans-serif; + color: #212529; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; +} + +/* Header */ +.navbar { + box-shadow: 0 2px 4px rgba(0,0,0,.05); +} + +.navbar-brand { + font-weight: 700; + color: #004A7F !important; +} + +/* Hero Section */ +.hero-section { + background-color: #F8F9FA; + padding: 6rem 0; + text-align: center; +} + +.hero-section h1 { + font-size: 3rem; + color: #004A7F; +} + +.hero-section .lead { + font-size: 1.25rem; + color: #6c757d; + margin-bottom: 2rem; +} + +/* Buttons */ +.btn-primary { + background-color: #004A7F; + border-color: #004A7F; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.btn-primary:hover { + background-color: #003359; + border-color: #003359; +} + +.btn-secondary { + background-color: #FDB813; + border-color: #FDB813; + color: #212529; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; +} + +.btn-secondary:hover { + background-color: #e0a000; + border-color: #e0a000; +} + + +/* Product Grid */ +.product-grid { + padding: 4rem 0; +} + +.product-grid h2 { + text-align: center; + margin-bottom: 3rem; + color: #004A7F; +} + +.product-card { + border: 1px solid #dee2e6; + border-radius: 0.25rem; + transition: box-shadow 0.3s ease, transform 0.3s ease; + background-color: #fff; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 15px rgba(0,0,0,.1); +} + +.product-card .card-img-top { + border-bottom: 1px solid #dee2e6; + aspect-ratio: 4 / 3; + object-fit: cover; +} + +.product-card .card-body { + text-align: center; +} + +.product-card .card-title { + font-size: 1.1rem; + color: #343a40; +} + +/* Footer */ +.footer { + background-color: #004A7F; + color: white; + padding: 3rem 0; +} + +.footer a { + color: #FDB813; +} + +.footer a:hover { + color: #fff; + text-decoration: none; +} + +.footer .social-icons a { + font-size: 1.5rem; + margin: 0 0.5rem; +} + +/* Product Page - Color Swatches */ +.color-swatches .swatch { + display: inline-block; + width: 32px; + height: 32px; + border-radius: 50%; + margin: 0 8px 8px 0; + cursor: pointer; + border: 2px solid #dee2e6; + transition: all 0.2s ease-in-out; + background-size: cover; + background-position: center; +} + +.color-swatches .swatch:hover { + transform: scale(1.1); + border-color: #6c757d; +} + +.color-swatches .swatch.active { + border-color: #004A7F; + transform: scale(1.1); + box-shadow: 0 0 8px rgba(0, 74, 127, 0.5); +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..f1b2b54 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,16 @@ +// Future JavaScript for interactivity + +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM fully loaded and parsed'); + + // Color swatch interactivity + const swatches = document.querySelectorAll('.color-swatches .swatch'); + swatches.forEach(swatch => { + swatch.addEventListener('click', function() { + // Remove active class from all swatches + swatches.forEach(s => s.classList.remove('active')); + // Add active class to the clicked swatch + this.classList.add('active'); + }); + }); +}); \ No newline at end of file diff --git a/db/config.php b/db/config.php index f12ebaf..f62c507 100644 --- a/db/config.php +++ b/db/config.php @@ -1,17 +1,42 @@ PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - } - return $pdo; -} +// --- Database Credentials --- +// --> IMPORTANT: Replace with your actual database credentials +define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1'); +define('DB_PORT', getenv('DB_PORT') ?: 3306); +define('DB_NAME', getenv('DB_NAME') ?: 'localdb'); +define('DB_USER', getenv('DB_USER') ?: 'user'); +define('DB_PASS', getenv('DB_PASS') ?: 'password'); + +/** + * Establishes a PDO database connection. + * + * @return PDO|null A PDO connection object on success, or null on failure. + */ +function db_connect() { + static $pdo = null; + + if ($pdo !== null) { + return $pdo; + } + + $dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=utf8mb4'; + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + return $pdo; + } catch (PDOException $e) { + // In a real application, you would log this error and show a generic message. + // For development, it's useful to see the error. + error_log('Database Connection Error: ' . $e->getMessage()); + // Never show detailed errors in production + // die('Database connection failed. Please check configuration.'); + return null; + } +} \ No newline at end of file diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..e938ba2 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,47 @@ +exec($statement); + echo "[SUCCESS] Executed: " . substr($statement, 0, 80) . "...\n"; + } catch (PDOException $e) { + echo "[ERROR] Failed to execute statement. \nError: " . $e->getMessage() . "\n"; + echo "Statement: \n" . $statement . "\n"; + } +} + +echo "\nMigration script finished.\n"; + diff --git a/db/migrate_users.php b/db/migrate_users.php new file mode 100644 index 0000000..56613fe --- /dev/null +++ b/db/migrate_users.php @@ -0,0 +1,36 @@ +exec("CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + );"); + + echo "Tabela 'users' criada com sucesso ou já existente.
"; + + // Verifica se o usuário admin já existe + $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute(['admin']); + if ($stmt->fetch()) { + echo "Usuário 'admin' já existe.
"; + } else { + // Cria o usuário admin padrão + $username = 'admin'; + $password = 'admin'; // Senha padrão + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + $stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); + $stmt->execute([$username, $hashed_password]); + echo "Usuário 'admin' criado com sucesso com a senha padrão 'admin'.
"; + } + +} catch (PDOException $e) { + die("Erro na migração do banco de dados: " . $e->getMessage()); +} +?> diff --git a/index.php b/index.php index 7205f3d..9b0b3ad 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,122 @@ - - - + + - - - New Style - - - - - - - - - - - - - - - - - - - + + + MECoding Studio - Catálogo de Produtos + + + + + + + + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
-
- Page updated: (UTC) -
+ + + + + +
+
+

Soluções Inovadoras em Expositores

+

Design e funcionalidade para valorizar seus produtos e impulsionar suas vendas.

+ Conheça Nossos Produtos +
+
+ + +
+

Produtos em Destaque

+
+ 'Estante Modular ', + 'image' => 'https://images.pexels.com/photos/5705080/pexels-photo-5705080.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ], + [ + 'title' => 'Display de Balcão', + 'image' => 'https://images.pexels.com/photos/1148820/pexels-photo-1148820.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ], + [ + 'title' => 'Gôndola Central', + 'image' => 'https://images.pexels.com/photos/271816/pexels-photo-271816.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ], + [ + 'title' => 'Estante de Parede', + 'image' => 'https://images.pexels.com/photos/2089698/pexels-photo-2089698.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ], + [ + 'title' => 'Mesa de Centro', + 'image' => 'https://images.pexels.com/photos/37347/office-sitting-room-executive-sitting.jpg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ], + [ + 'title' => 'Estante de Livros', + 'image' => 'https://images.pexels.com/photos/159844/cellular-education-classroom-159844.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' + ] + ]; + + $is_first = true; + foreach ($products as $product) { + $link = $is_first ? 'product.php' : '#'; + echo ' +
+
+ ' . htmlspecialchars($product['title']) . ' +
+
' . htmlspecialchars($product['title']) . '
+ Ver Detalhes +
+
+
'; + $is_first = false; + } + ?> +
+
+ + + + + + + + - + \ No newline at end of file diff --git a/product.php b/product.php new file mode 100644 index 0000000..699971f --- /dev/null +++ b/product.php @@ -0,0 +1,184 @@ + + + + + + Detalhes do Produto - MECoding Studio + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ Estante Modular - Vista Principal +
+
+ + + +
+
+ + +
+

Estante Modular Flex-1000

+

SKU: EM-FLX-1000

+

Uma solução versátil e robusta para otimizar seu espaço comercial com elegância e praticidade.

+ +
+
Cor/Acabamento
+
+ + + +
+ Nota: Para a cor "Madeira", adicione uma imagem em `assets/images/textures/wood.jpg` +
+ +
+
Dimensões (cm)
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+

A Estante Modular Flex-1000 é a escolha ideal para quem busca flexibilidade e design moderno. Construída com materiais de alta durabilidade, ela se adapta a diferentes layouts de loja, desde boutiques a grandes magazines. As prateleiras ajustáveis permitem a exposição de uma vasta gama de produtos, enquanto o acabamento refinado valoriza o ambiente e a apresentação dos itens.

+
    +
  • Estrutura em aço com pintura eletrostática.
  • +
  • Prateleiras em MDF com altura regulável.
  • +
  • Montagem simples e rápida.
  • +
  • Pés niveladores para maior estabilidade.
  • +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Material da EstruturaAço Carbono
Material das PrateleirasMDF 18mm
Capacidade de Carga por Prateleira35kg
Peso Total45kg
Garantia2 anos
+
+
+

Faça o download dos materiais de apoio para mais informações.

+ +
+
+
+ +
+ + + + + + + + + +