commit a5d273c8fcd09f2604e5afe79f7a027cd3d67796 Author: jrosh Date: Wed Feb 11 00:12:51 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bef1b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +package-lock.json +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfadd19 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +AhojSvet Landing page \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..4925a63 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Ahoj Svet! + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3048d58 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "ahojsvet.landing", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "i18next": "^25.8.4", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.5.4", + "react-router-dom": "^7.13.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "vite": "^7.2.4" + } +} diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..fe64cad Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000..e01124b Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/api/contact.php b/public/api/contact.php new file mode 100644 index 0000000..f870a9c --- /dev/null +++ b/public/api/contact.php @@ -0,0 +1,68 @@ +load(); + +header('Content-Type: application/json'); +header('Access-Control-Allow-Origin: *'); +header('Access-Control-Allow-Methods: POST'); +header('Access-Control-Allow-Headers: Content-Type'); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + + // Check honeypot + if (!empty($data['website']) || !empty($data['phone_check'])) { + http_response_code(200); + echo json_encode(['success' => true]); + exit; + } + + // Validate and sanitize + $name = htmlspecialchars($data['name']); + $email = filter_var($data['email'], FILTER_SANITIZE_EMAIL); + $company = htmlspecialchars($data['company']); + $type = htmlspecialchars($data['type']); + $message = htmlspecialchars($data['message']); + + try { + $mail = new PHPMailer(true); + + // SMTP Configuration + $mail->isSMTP(); + $mail->Host = $_ENV['SMTP_HOST']; + $mail->SMTPAuth = true; + $mail->Username = $_ENV['SMTP_USERNAME']; + $mail->Password = $_ENV['SMTP_PASSWORD']; + $mail->SMTPSecure = $_ENV['SMTP_ENCRYPTION']; + $mail->Port = $_ENV['SMTP_PORT']; + + // Recipients + $mail->setFrom($_ENV['SMTP_FROM_EMAIL'], $_ENV['SMTP_FROM_NAME']); + $mail->addAddress($_ENV['CONTACT_EMAIL']); + $mail->addReplyTo($email, $name); + + // Content + $mail->isHTML(false); + $mail->Subject = 'New Contact Form: ' . $type; + $mail->Body = "Name: $name\nEmail: $email\nCompany: $company\nType: $type\n\nMessage:\n$message"; + + $mail->send(); + + http_response_code(200); + echo json_encode(['success' => true]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $mail->ErrorInfo]); + } +} else { + http_response_code(405); + echo json_encode(['error' => 'Method not allowed']); +} +?> \ No newline at end of file diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..cf6b286 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/dashboard.png b/public/dashboard.png new file mode 100644 index 0000000..04ee9e7 Binary files /dev/null and b/public/dashboard.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..e258c1b Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..df1b5cb Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..fb89b3f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/katana.svg b/public/katana.svg new file mode 100644 index 0000000..1a2119b --- /dev/null +++ b/public/katana.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..996f5f8 --- /dev/null +++ b/src/App.css @@ -0,0 +1,117 @@ +* { + font-family: 'Work Sans', sans-serif; +} + +.gradient-text { + background: linear-gradient(135deg, #fff 0%, #fca5a5 50%, #b91c1c 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glow { + box-shadow: 0 0 60px rgba(185, 28, 28, 0.3); +} + +.card-hover { + transition: all 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +.float { + animation: float 6s ease-in-out infinite; +} + +@keyframes pulse-slow { + 0%, + 100% { + opacity: 0.4; + } + 50% { + opacity: 0.8; + } +} + +.pulse-slow { + animation: pulse-slow 4s ease-in-out infinite; +} + +@keyframes slash { + 0% { + transform: scaleX(1); + opacity: 0.6; + } + 50% { + transform: scaleX(1.05); + opacity: 1; + } + 100% { + transform: scaleX(1); + opacity: 0.6; + } +} + +#root { + margin: 0 auto; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +.hidden-honeypot { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + + diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..4b88ef4 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Navigation from './components/Navigation'; +import Hero from './components/Hero'; +import TheProblem from './components/TheProblem'; +import Features from './components/Features'; +import SpeedComparison from './components/SpeedComparison'; +import HiddenCostsIcebergSection from './components/HiddenCostsIcebergSection'; +import Screenshots from './components/Screenshots'; +import InTheWild from './components/InTheWild'; +import ContactForm from './components/ContactForm'; +import CTA from './components/CTA'; +import Footer from './components/Footer'; +import './App.css'; + +export default function App() { + const [scrollY, setScrollY] = useState(0); + const { i18n } = useTranslation(); + + useEffect(() => { + const handleScroll = () => setScrollY(window.scrollY); + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return ( +
+ + + + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/CTA.jsx b/src/components/CTA.jsx new file mode 100644 index 0000000..7ca2070 --- /dev/null +++ b/src/components/CTA.jsx @@ -0,0 +1,29 @@ +import { useTranslation } from 'react-i18next'; +import { Play } from 'lucide-react'; + +export default function CTA() { + const { t } = useTranslation(); + const cta = t('cta', { returnObjects: true }); + + return ( +
+
+
+
+
+

+ {cta.title} +

+

+ {cta.subtitle} +

+ + {cta.button} + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ContactForm.jsx b/src/components/ContactForm.jsx new file mode 100644 index 0000000..ab40d6b --- /dev/null +++ b/src/components/ContactForm.jsx @@ -0,0 +1,157 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function ContactForm() { + const { t } = useTranslation(); + const contact = t('contact', { returnObjects: true }); + const [formSent, setFormSent] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + company: '', + type: contact.types[0], + message: '', + website: '', + phone_check: '' + }); + + const handleSubmit = () => { + // Check honeypot fields + if (formData.website || formData.phone_check) { + return; // Ignore if honeypot fields are filled + } + + // TODO: Replace with actual form submission endpoint + console.log('Form submitted:', formData); + setFormSent(true); + setTimeout(() => setFormSent(false), 3000); + setFormData({ + name: '', + email: '', + company: '', + type: contact.types[0], + message: '', + website: '', + phone_check: '' + }); + }; + + return ( +
+
+
+

+ {contact.title} +

+

+ {contact.subtitle} +

+
+ +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-red-700 focus:outline-none transition-colors" + /> +
+
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-red-700 focus:outline-none transition-colors" + /> +
+
+ +
+
+ + setFormData({ ...formData, company: e.target.value })} + className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-red-700 focus:outline-none transition-colors" + /> +
+
+ + +
+
+ +
+ +