= htmlspecialchars($service[1], ENT_QUOTES, 'UTF-8') ?>
+= htmlspecialchars($service[2], ENT_QUOTES, 'UTF-8') ?>
+diff --git a/api/contact.php b/api/contact.php new file mode 100644 index 0000000..19f14e7 --- /dev/null +++ b/api/contact.php @@ -0,0 +1,108 @@ + false, 'message' => 'Método não permitido.']); +} + +$name = trim((string)($_POST['name'] ?? '')); +$email = trim((string)($_POST['email'] ?? '')); +$phone = trim((string)($_POST['phone'] ?? '')); +$projectType = trim((string)($_POST['project_type'] ?? '')); +$message = trim((string)($_POST['message'] ?? '')); +$honeypot = trim((string)($_POST['website'] ?? '')); + +if ($honeypot !== '') { + respond(200, ['success' => true, 'message' => 'Pedido recebido. Obrigado.']); +} + +$errors = []; +if (mb_strlen($name) < 2 || mb_strlen($name) > 120) $errors[] = 'Indica um nome válido.'; +if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 160) $errors[] = 'Indica um email válido.'; +if ($phone !== '' && mb_strlen($phone) > 40) $errors[] = 'O telemóvel é demasiado longo.'; +if ($projectType === '' || mb_strlen($projectType) > 80) $errors[] = 'Selecciona o tipo de projecto.'; +if (mb_strlen($message) < 20 || mb_strlen($message) > 1800) $errors[] = 'A mensagem deve ter entre 20 e 1800 caracteres.'; +if ($errors) { + respond(422, ['success' => false, 'message' => implode(' ', $errors)]); +} + +$recipient = 'ola@paulogomes.pt'; +$siteName = 'pg Web e APP Designer'; +$emailSent = false; +$submissionId = null; + +try { + require_once __DIR__ . '/../db/config.php'; + $pdo = db(); + $pdo->exec("CREATE TABLE IF NOT EXISTS contact_requests ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL, + phone VARCHAR(40) NULL, + project_type VARCHAR(80) NOT NULL, + message TEXT NOT NULL, + email_sent TINYINT(1) NOT NULL DEFAULT 0, + status VARCHAR(30) NOT NULL DEFAULT 'new', + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(255) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + + $stmt = $pdo->prepare('INSERT INTO contact_requests (name, email, phone, project_type, message, ip_address, user_agent) VALUES (:name, :email, :phone, :project_type, :message, :ip, :user_agent)'); + $stmt->execute([ + ':name' => $name, + ':email' => $email, + ':phone' => $phone !== '' ? $phone : null, + ':project_type' => $projectType, + ':message' => $message, + ':ip' => substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45) ?: null, + ':user_agent' => substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255) ?: null, + ]); + $submissionId = (int)$pdo->lastInsertId(); +} catch (Throwable $e) { + error_log('Contact DB error: ' . $e->getMessage()); +} + +try { + require_once __DIR__ . '/../mail/MailService.php'; + $safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safePhone = htmlspecialchars($phone !== '' ? $phone : 'Não indicado', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeProject = htmlspecialchars($projectType, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeMessage = nl2br(htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); + $html = "
Nome: {$safeName}
" + . "Email: {$safeEmail}
" + . "Telemóvel: {$safePhone}
" + . "Tipo de projecto: {$safeProject}
" + . "Website: {$siteName}
" + . "Detalhe da mensagem:
{$safeMessage}
"; + $text = "Novo pedido — {$siteName}\nNome: {$name}\nEmail: {$email}\nTelemóvel: " . ($phone !== '' ? $phone : 'Não indicado') . "\nTipo de projecto: {$projectType}\n\nDetalhe da mensagem:\n{$message}"; + $result = MailService::sendMail($recipient, $siteName . ' — novo pedido de contacto', $html, $text, ['reply_to' => $email]); + $emailSent = !empty($result['success']); + if (!$emailSent) error_log('Contact mail error: ' . ($result['error'] ?? 'unknown')); +} catch (Throwable $e) { + error_log('Contact mail exception: ' . $e->getMessage()); +} + +if ($submissionId) { + try { + $stmt = db()->prepare('UPDATE contact_requests SET email_sent = :email_sent WHERE id = :id'); + $stmt->execute([':email_sent' => $emailSent ? 1 : 0, ':id' => $submissionId]); + } catch (Throwable $e) { + error_log('Contact DB update error: ' . $e->getMessage()); + } +} + +$messageText = $emailSent + ? 'Mensagem enviada com sucesso. Obrigado — responderei assim que possível.' + : 'Pedido guardado. Se o envio por email não estiver configurado no servidor, contacta directamente ola@paulogomes.pt.'; + +respond(200, ['success' => true, 'message' => $messageText]); diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..4aedc73 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,431 @@ +:root { + --bg: #080808; + --surface: rgba(14, 14, 14, 0.86); + --surface-strong: #101010; + --text: #f5f5f1; + --muted: #a7a7a0; + --line: rgba(255, 255, 255, 0.12); + --line-strong: rgba(255, 255, 255, 0.22); + --accent: #ffffff; + --accent-text: #050505; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 18px; + --space: 1rem; +} + +[data-bs-theme="light"] { + --bg: #ffffff; + --surface: rgba(255, 255, 255, 0.88); + --surface-strong: #f6f6f3; + --text: #050505; + --muted: #60605b; + --line: rgba(0, 0, 0, 0.12); + --line-strong: rgba(0, 0, 0, 0.2); + --accent: #050505; + --accent-text: #ffffff; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + text-rendering: optimizeLegibility; +} +a { color: inherit; } +.skip-link { + position: absolute; + left: 1rem; + top: -4rem; + z-index: 999; + background: var(--accent); + color: var(--accent-text); + padding: .65rem .85rem; + border-radius: var(--radius-sm); + transition: top .2s ease; +} +.skip-link:focus { top: 1rem; } +.site-shell { position: relative; min-height: 100vh; overflow-x: clip; } +.hero-video { + position: fixed; + top: 300px; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: calc(100vh - 300px); + object-fit: cover; + opacity: 0; + z-index: 0; + filter: grayscale(1) contrast(1.08) brightness(.62); + pointer-events: none; +} +.video-scrim { + position: fixed; + inset: 0; + z-index: 1; + background: rgba(0, 0, 0, .46); + pointer-events: none; +} +[data-bs-theme="light"] .video-scrim { background: rgba(255, 255, 255, .58); } +.site-header { + z-index: 10; + background: rgba(8, 8, 8, .72); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(18px); +} +[data-bs-theme="light"] .site-header { background: rgba(255,255,255,.74); } +.navbar { --bs-navbar-color: var(--muted); --bs-navbar-hover-color: var(--text); --bs-navbar-active-color: var(--text); } +.brand-mark { + color: var(--text); + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(1.35rem, 2vw, 1.9rem); + line-height: 1; + letter-spacing: -.045em; +} +.brand-mark sup { font-family: Inter, sans-serif; font-size: .42em; top: -.9em; margin-left: .08rem; } +.nav-link { font-size: .9rem; color: var(--muted); transition: color .2s ease; } +.nav-link:hover, .nav-link:focus, .nav-link.active { color: var(--text); } +.navbar-toggler { border: 1px solid var(--line); border-radius: var(--radius-sm); } +.btn { border-radius: var(--radius-sm); transition: transform .18s ease, background-color .18s ease, border-color .18s ease; } +.btn:hover { transform: translateY(-1px) scale(1.015); } +.btn-primary-dark { + background: var(--accent); + color: var(--accent-text); + border: 1px solid var(--accent); + font-weight: 650; + padding: .72rem 1.15rem; +} +.btn-primary-dark:hover, .btn-primary-dark:focus { background: var(--accent); color: var(--accent-text); border-color: var(--accent); } +.btn-outline-soft { color: var(--text); border: 1px solid var(--line-strong); background: transparent; padding: .72rem 1.15rem; } +.btn-outline-soft:hover { border-color: var(--accent); color: var(--text); } +.btn-ghost { color: var(--muted); border: 1px solid var(--line); background: transparent; } +.btn-ghost:hover { color: var(--text); border-color: var(--line-strong); } +.hero-section { + position: relative; + z-index: 2; + padding-top: calc(8rem - 75px); + padding-bottom: 10rem; +} +.eyebrow { + color: var(--muted); + font-size: .76rem; + font-weight: 700; + letter-spacing: .12em; + text-transform: uppercase; + margin-bottom: 1rem; +} +.hero-title { + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(3.4rem, 8.4vw, 8.25rem); + font-weight: 400; + line-height: .95; + letter-spacing: -0.045em; + max-width: 1180px; + margin: 0 auto; +} +.hero-copy { + color: var(--muted); + font-size: clamp(1rem, 1.3vw, 1.18rem); + line-height: 1.75; + max-width: 720px; + margin-top: 1.75rem; +} +.hero-actions { margin-top: 2.5rem; } +.stats-panel { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1px; + max-width: 820px; + margin: 5rem auto 0; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + overflow: hidden; + background: var(--line); +} +.stats-panel div { + background: var(--surface); + padding: 1.35rem 1rem; + backdrop-filter: blur(18px); +} +.stats-panel strong { + display: block; + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(2.3rem, 5vw, 4.2rem); + font-weight: 400; + line-height: 1; +} +.stats-panel span { display: block; color: var(--muted); font-size: .9rem; margin-top: .35rem; } +.section-pad { position: relative; z-index: 2; padding: 6.5rem 0; } +.section-quiet { background: rgba(255,255,255,.025); border-block: 1px solid var(--line); } +[data-bs-theme="light"] .section-quiet { background: rgba(0,0,0,.025); } +.section-heading { max-width: 760px; margin-bottom: 2.5rem; } +.section-heading.compact { margin-inline: auto; text-align: center; } +.section-heading h2, .section-title, .contact-card h2 { + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(2.45rem, 5vw, 5.25rem); + line-height: .98; + letter-spacing: -.04em; + font-weight: 400; +} +.section-heading p:not(.eyebrow), .section-copy { color: var(--muted); line-height: 1.75; } +.service-card, .contact-card, .form-card, .timeline > div { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-md); + backdrop-filter: blur(18px); +} +.service-card { padding: 1.35rem; transition: transform .2s ease, border-color .2s ease; } +.service-card:hover { transform: translateY(-4px); border-color: var(--line-strong); } +.service-card span { color: var(--muted); font-size: .78rem; font-weight: 700; letter-spacing: .12em; } +.service-card h3 { margin: 1.4rem 0 .8rem; font-size: 1.03rem; font-weight: 750; } +.service-card p { color: var(--muted); line-height: 1.7; font-size: .94rem; margin: 0; } +.tool-grid { display: flex; flex-wrap: wrap; gap: .65rem; } +.tool-grid span { + border: 1px solid var(--line); + background: var(--surface); + color: var(--text); + border-radius: var(--radius-sm); + padding: .7rem .9rem; + font-size: .92rem; +} +.timeline { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; margin-top: 2rem; } +.timeline > div { padding: 1.25rem; } +.timeline strong { color: var(--muted); font-size: .78rem; letter-spacing: .12em; } +.timeline h3 { font-size: 1.04rem; margin: 1rem 0 .6rem; } +.timeline p { color: var(--muted); margin: 0; line-height: 1.65; } +.contact-section { padding-bottom: 7rem; } +.contact-card, .form-card { padding: clamp(1.25rem, 3vw, 2rem); } +.contact-list { list-style: none; padding: 0; margin: 2rem 0 0; display: grid; gap: 1rem; } +.contact-list li { border-top: 1px solid var(--line); padding-top: 1rem; } +.contact-list span { display: block; color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .11em; margin-bottom: .35rem; } +.contact-list a, .contact-list strong { font-size: 1.06rem; text-decoration: none; font-weight: 650; } +.contact-list small { display: block; color: var(--muted); margin-top: .25rem; } +.form-label { font-size: .86rem; font-weight: 700; color: var(--text); } +.form-label span, .form-text, .muted-note { color: var(--muted); } +.form-control, .form-select { + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background-color: rgba(255,255,255,.04); + color: var(--text); + min-height: 3rem; +} +[data-bs-theme="light"] .form-control, [data-bs-theme="light"] .form-select { background-color: rgba(0,0,0,.035); } +.form-control:focus, .form-select:focus { + color: var(--text); + background-color: transparent; + border-color: var(--line-strong); + box-shadow: 0 0 0 .2rem rgba(128,128,128,.14); +} +.form-control::placeholder { color: color-mix(in srgb, var(--muted) 75%, transparent); } +.secure-pill { border: 1px solid var(--line); border-radius: 999px; padding: .4rem .6rem; color: var(--muted); font-size: .78rem; white-space: nowrap; } +.site-footer { position: relative; z-index: 2; border-top: 1px solid var(--line); color: var(--muted); padding: 1.25rem 0; font-size: .86rem; } +.toast { background: var(--surface-strong); color: var(--text); border: 1px solid var(--line); } +.animate-rise { opacity: 0; transform: translateY(20px); animation: fade-rise .8s ease-out forwards; } +.delay-1 { animation-delay: .2s; } +.delay-2 { animation-delay: .4s; } +@keyframes fade-rise { to { opacity: 1; transform: translateY(0); } } +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .animate-rise { animation: none; opacity: 1; transform: none; } + .btn, .service-card { transition: none; } +} +@media (max-width: 991.98px) { + .site-header .navbar-collapse { padding-top: 1rem; } + .timeline { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 575.98px) { + .hero-section { padding-top: 7rem; padding-bottom: 6rem; } + .stats-panel { grid-template-columns: 1fr; margin-top: 3rem; } + .section-pad { padding: 4.5rem 0; } + .timeline { grid-template-columns: 1fr; } + .hero-video { top: 220px; height: calc(100vh - 220px); } } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; + +.portfolio-section { overflow: hidden; } +.project-featured, .project-card, .testimonial-card, .cookie-banner { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + backdrop-filter: blur(18px); } - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.project-featured { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(320px, .92fr); + gap: clamp(1.5rem, 4vw, 3rem); + align-items: center; + padding: clamp(1.1rem, 3vw, 2rem); + box-shadow: 0 30px 90px rgba(0,0,0,.22); } - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.project-visual, .project-thumb { + position: relative; + overflow: hidden; + border: 1px solid var(--line); + background: #111; + isolation: isolate; } - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.project-visual { min-height: 360px; border-radius: var(--radius-lg); } +.project-visual::before, .project-thumb::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient(135deg, rgba(255,255,255,.16), transparent 45%), radial-gradient(circle at 18% 18%, rgba(255,255,255,.24), transparent 28%); } - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.real-estate-visual { background: radial-gradient(circle at 20% 20%, rgba(255,255,255,.28), transparent 30%), linear-gradient(135deg, #101010, #2f332f 50%, #090909); } +.visual-orb { + position: absolute; + width: 190px; + height: 190px; + right: 8%; + top: 8%; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, #fff, #8d9388 45%, #222 74%); + filter: blur(.1px); + opacity: .72; } - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.mock-browser { + position: absolute; + left: 8%; + bottom: 10%; + width: min(72%, 420px); + min-height: 250px; + padding: 1rem; + border-radius: 18px; + background: rgba(255,255,255,.09); + border: 1px solid rgba(255,255,255,.18); + box-shadow: 0 25px 80px rgba(0,0,0,.34); + backdrop-filter: blur(16px); } - -::-webkit-scrollbar-track { - background: transparent; +.mock-top { display: flex; gap: .35rem; margin-bottom: 1rem; } +.mock-top span { width: .58rem; height: .58rem; border-radius: 50%; background: rgba(255,255,255,.55); } +.mock-hero { height: 92px; border-radius: 14px; background: linear-gradient(135deg, rgba(255,255,255,.72), rgba(255,255,255,.12)); } +.mock-lines { display: grid; gap: .6rem; margin-top: 1rem; } +.mock-lines i { display: block; height: .55rem; border-radius: 999px; background: rgba(255,255,255,.22); } +.mock-lines i:nth-child(2) { width: 72%; } +.mock-lines i:nth-child(3) { width: 54%; } +.project-visual > strong { + position: absolute; + right: 1rem; + bottom: .65rem; + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(4rem, 10vw, 8rem); + font-weight: 400; + line-height: 1; + letter-spacing: -.06em; + color: rgba(255,255,255,.92); } - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; +.project-content { padding: clamp(.25rem, 2vw, 1rem); } +.project-type { + display: inline-flex; + align-items: center; + width: fit-content; + color: var(--muted); + border: 1px solid var(--line); + border-radius: 999px; + padding: .42rem .68rem; + font-size: .74rem; + font-weight: 750; + letter-spacing: .11em; + text-transform: uppercase; } - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.project-content h3, .project-card h3 { + font-family: "Instrument Serif", Georgia, serif; + font-weight: 400; + letter-spacing: -.035em; + line-height: 1; } - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +.project-content h3 { font-size: clamp(2.5rem, 5vw, 5rem); margin: 1.3rem 0 1rem; } +.project-card h3 { font-size: clamp(1.9rem, 3vw, 2.55rem); margin: 1rem 0 .75rem; } +.project-content p, .project-card p { color: var(--muted); line-height: 1.72; } +.project-metric { + display: inline-grid; + grid-template-columns: auto 1fr; + gap: .7rem; + align-items: center; + margin: .4rem 0 1.4rem; } - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } +.project-metric strong { font-family: "Instrument Serif", Georgia, serif; font-size: 3.2rem; font-weight: 400; line-height: 1; } +.project-metric span { color: var(--muted); max-width: 170px; line-height: 1.2; } +.project-link { + display: inline-flex; + align-items: center; + gap: .4rem; + color: var(--text); + font-weight: 750; + text-decoration: none; + border-bottom: 1px solid var(--line-strong); + padding-bottom: .2rem; } - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +.project-link::after { content: "↗"; font-size: .86rem; transition: transform .18s ease; } +.project-link:hover::after { transform: translate(2px, -2px); } +.project-card { padding: 1rem; transition: transform .2s ease, border-color .2s ease; } +.project-card:hover { transform: translateY(-5px); border-color: var(--line-strong); } +.project-thumb { min-height: 190px; border-radius: var(--radius-md); margin-bottom: 1rem; display: grid; place-items: center; } +.project-thumb span { + position: relative; + z-index: 2; + font-family: "Instrument Serif", Georgia, serif; + font-size: clamp(3rem, 7vw, 5rem); + line-height: 1; + letter-spacing: -.06em; + color: rgba(255,255,255,.94); } - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.project-thumb i { + position: absolute; + width: 130px; + height: 130px; + border-radius: 28px; + background: linear-gradient(145deg, rgba(255,255,255,.7), rgba(255,255,255,.04)); + transform: rotate(18deg) translate(28px, 12px); + box-shadow: 0 30px 70px rgba(0,0,0,.26); } - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.fitflow-visual { background: linear-gradient(135deg, #101010, #24352d 48%, #0d1611); } +.community-visual { background: radial-gradient(circle at 25% 20%, rgba(255,255,255,.25), transparent 28%), linear-gradient(135deg, #111, #29323a 55%, #090b0d); } +.ocr-visual { background: linear-gradient(135deg, #0a0a0a, #263144 52%, #0c0e16); } +.moto-visual { background: radial-gradient(circle at 75% 18%, rgba(255,255,255,.26), transparent 26%), linear-gradient(135deg, #111, #3a3028 52%, #100d0a); } +.testimonial-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 2rem; } +.testimonial-card { padding: clamp(1.25rem, 3vw, 1.75rem); } +.testimonial-card blockquote { margin: 0; color: var(--text); font-size: clamp(1.05rem, 1.5vw, 1.28rem); line-height: 1.65; } +.testimonial-card footer { border-top: 1px solid var(--line); margin-top: 1.4rem; padding-top: 1rem; } +.testimonial-card strong { display: block; font-weight: 750; } +.testimonial-card span { display: block; color: var(--muted); font-size: .9rem; margin-top: .18rem; } +.cookie-banner { + position: fixed; + left: 1rem; + right: 1rem; + bottom: 1rem; + z-index: 1080; + max-width: 980px; + margin: 0 auto; + padding: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + box-shadow: 0 24px 80px rgba(0,0,0,.28); + opacity: 0; + transform: translateY(18px); + pointer-events: none; + transition: opacity .22s ease, transform .22s ease; } - -.chat-input-area form { - display: flex; - gap: 0.75rem; +.cookie-banner.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; } +.cookie-banner strong { display: block; margin-bottom: .3rem; } +.cookie-banner p { color: var(--muted); margin: 0; line-height: 1.55; font-size: .92rem; } +.cookie-actions { display: flex; flex-wrap: wrap; gap: .55rem; justify-content: flex-end; min-width: max-content; } +@media (max-width: 991.98px) { + .project-featured { grid-template-columns: 1fr; } + .testimonial-grid { grid-template-columns: 1fr; } + .cookie-banner { align-items: flex-start; flex-direction: column; } + .cookie-actions { width: 100%; justify-content: flex-start; min-width: 0; } } - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; +@media (max-width: 575.98px) { + .project-visual { min-height: 280px; } + .mock-browser { width: 78%; min-height: 210px; } + .project-metric { grid-template-columns: 1fr; } + .cookie-actions .btn { width: 100%; } } - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/css/fonts.css b/assets/css/fonts.css new file mode 100644 index 0000000..25ec514 --- /dev/null +++ b/assets/css/fonts.css @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&display=swap'); diff --git a/assets/images/favicon.svg b/assets/images/favicon.svg new file mode 100644 index 0000000..5518860 --- /dev/null +++ b/assets/images/favicon.svg @@ -0,0 +1,13 @@ + diff --git a/assets/js/main.js b/assets/js/main.js index d349598..050c328 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,122 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +(() => { + const root = document.body; + const themeToggle = document.getElementById('themeToggle'); + const storedTheme = localStorage.getItem('pg-theme'); + if (storedTheme === 'light' || storedTheme === 'dark') { + root.setAttribute('data-bs-theme', storedTheme); + } + const syncThemeButton = () => { + if (!themeToggle) return; + themeToggle.textContent = root.getAttribute('data-bs-theme') === 'dark' ? 'Modo claro' : 'Modo escuro'; + }; + syncThemeButton(); + themeToggle?.addEventListener('click', () => { + const next = root.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark'; + root.setAttribute('data-bs-theme', next); + localStorage.setItem('pg-theme', next); + syncThemeButton(); + }); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + const video = document.getElementById('heroVideo'); + let rafId = null; + const fadeWindow = 0.5; + const monitorVideo = () => { + if (video && Number.isFinite(video.duration) && video.duration > 0) { + const current = video.currentTime || 0; + const duration = video.duration; + let opacity = 1; + if (current < fadeWindow) opacity = current / fadeWindow; + if (duration - current < fadeWindow) opacity = Math.max(0, (duration - current) / fadeWindow); + video.style.opacity = String(Math.max(0, Math.min(1, opacity * 0.7))); + } + rafId = requestAnimationFrame(monitorVideo); + }; + if (video) { + video.addEventListener('loadedmetadata', () => { + video.currentTime = 0; + video.play().catch(() => {}); + if (!rafId) monitorVideo(); }); -}); + video.addEventListener('ended', () => { + video.style.opacity = '0'; + window.setTimeout(() => { + video.currentTime = 0; + video.play().catch(() => {}); + }, 100); + }); + video.load(); + } + + const parallaxItems = [video, document.querySelector('.stats-panel')].filter(Boolean); + const handleParallax = () => { + const y = window.scrollY || 0; + parallaxItems.forEach((item, idx) => { + const speed = idx === 0 ? 0.08 : -0.025; + item.style.transform = `translate3d(0, ${y * speed}px, 0)`; + }); + }; + if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + window.addEventListener('scroll', handleParallax, { passive: true }); + handleParallax(); + } + + const form = document.getElementById('contactForm'); + const alertBox = document.getElementById('formAlert'); + const submitBtn = document.getElementById('submitBtn'); + const toastEl = document.getElementById('siteToast'); + const toastBody = document.getElementById('toastBody'); + const showAlert = (type, message) => { + if (!alertBox) return; + alertBox.className = `alert alert-${type}`; + alertBox.textContent = message; + }; + const showToast = (message) => { + if (toastBody) toastBody.textContent = message; + if (toastEl && window.bootstrap) new bootstrap.Toast(toastEl).show(); + }; + form?.addEventListener('submit', async (event) => { + event.preventDefault(); + showAlert('secondary', 'A enviar o teu pedido...'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = 'A enviar...'; + } + try { + const response = await fetch('api/contact.php', { + method: 'POST', + headers: { 'Accept': 'application/json' }, + body: new FormData(form) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || !data.success) { + showAlert('warning', data.message || 'Não foi possível enviar. Revê os campos e tenta novamente.'); + return; + } + form.reset(); + showAlert('success', data.message || 'Mensagem enviada com sucesso.'); + showToast('Pedido recebido. Obrigado!'); + } catch (error) { + showAlert('danger', 'Erro de ligação. Tenta novamente dentro de instantes.'); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = 'Enviar mensagem'; + } + } + }); + + + const cookieBanner = document.getElementById("cookieBanner"); + const cookieAccept = document.getElementById("cookieAccept"); + const cookieDecline = document.getElementById("cookieDecline"); + const cookieChoice = localStorage.getItem("pg-cookie-choice"); + const closeCookieBanner = (choice) => { + localStorage.setItem("pg-cookie-choice", choice); + cookieBanner?.classList.remove("is-visible"); + }; + if (cookieBanner && !cookieChoice) { + window.setTimeout(() => cookieBanner.classList.add("is-visible"), 650); + } + cookieAccept?.addEventListener("click", () => closeCookieBanner("accepted")); + cookieDecline?.addEventListener("click", () => closeCookieBanner("declined")); +})(); diff --git a/index.php b/index.php index 7205f3d..9d9e88c 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,346 @@ - + - - -Websites · Web Apps · Mobile UI
+Crio experiências digitais para marcas, freelancers e negócios que querem transformar uma primeira impressão num pedido de contacto real.
+O que eu faço
+Da estratégia à manutenção, cada fase é tratada com detalhe para que o resultado seja bonito, rápido e fácil de usar.
+= htmlspecialchars($service[2], ENT_QUOTES, 'UTF-8') ?>
+Portfólio
+Uma selecção curta com exactamente os projectos principais: website, web apps, comunidade e IA. Sem projectos extra.
+Redesenho completo do web site e da experiência digital para tornar a presença online mais rápida, clara e orientada à captação de contactos.
+Experiência digital para acompanhar rotinas, progresso e fluxos de treino com uma interface limpa.
+ Abrir Web App +Plataforma comunitária para aproximar pessoas, objectos perdidos e informação útil num só lugar.
+ Ver comunidade +Interface orientada à conversão de imagens com texto em conteúdo legível, simples e directo.
+ Ver web site de IA +Web app para organizar viagens, memórias e aventuras em duas rodas com estética editorial.
+ Ver Moto trips +Ferramentas de desenvolvimento
+Escolho tecnologia consoante o projecto: protótipos rápidos, websites em alojamento próprio, aplicações React ou integrações com PHP/MySQL.
+Processo
+Entendo o negócio, objectivos, referências e restrições.
Defino estrutura, conteúdo, navegação e prioridades visuais.
Implemento, testo responsividade e preparo a publicação.
Recolho feedback e planeio melhorias após o lançamento.
Testemunhos
+"O redesenho tornou a navegação mais simples, a marca mais clara e a comunicação muito mais profissional."+ +
"A página ficou rápida, bonita e fácil de usar. O processo foi directo e sempre bem explicado."+ +
"Transformou uma ideia pouco estruturada numa web app com identidade, fluxo claro e pronta a evoluir."+ +
Contacto
+Envia uma mensagem com o essencial. Recebes resposta para marcar uma conversa e percebermos o melhor ponto de partida.
+= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-This page will update automatically as the plan is implemented.
-Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>