Initial commit

This commit is contained in:
dk
2025-12-29 15:04:33 +02:00
commit 62d0b7b3fa
60 changed files with 1992 additions and 0 deletions

1
.env.production Normal file
View File

@@ -0,0 +1 @@
DISABLE_ESLINT_PLUGIN=true

View File

@@ -0,0 +1,62 @@
name: Build
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to build'
required: true
default: 'main'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout branch
uses: actions/checkout@v3
with:
ref: ${{ gitea.event.inputs.branch }}
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 24
- name: Install dependencies
run: |
set -euo pipefail
npm install --no-audit --silent 2>&1 | tee install.log
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Build (react-scripts build)
env:
CI: 'false'
NODE_OPTIONS: '--max-old-space-size=4096'
run: |
set -euo pipefail
npm run build 2>&1 | tee build.log
timeout-minutes: 5
- name: Verify build folder exists
run: test -d build || (echo "No build folder. Check build logs above."; exit 1)
- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-logs
path: |
install.log
build.log
npm-debug.log*
if-no-files-found: ignore
- name: Build completed
if: success()
run: echo "Build completed successfully"

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "bulls-bikes-website",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"lucide-react": "^0.400.0",
"framer-motion": "^11.0.0"
},
"devDependencies": {
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWF0IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMTcwLjdoNTEydjE3MC42SDB6Ii8+CiAgPHBhdGggZmlsbD0iI2M4MTAyZSIgZD0iTTAgMGg1MTJ2MTcwLjdIMHptMCAzNDEuM2g1MTJWNTEySDB6Ii8+Cjwvc3ZnPgo=

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWJlIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2Utd2lkdGg9IjFwdCI+CiAgICA8cGF0aCBmaWxsPSIjMDAwMDAxIiBkPSJNMCAwaDE3MC43djUxMkgweiIvPgogICAgPHBhdGggZmlsbD0iI2ZmZDkwYyIgZD0iTTE3MC43IDBoMTcwLjZ2NTEySDE3MC43eiIvPgogICAgPHBhdGggZmlsbD0iI2YzMTgzMCIgZD0iTTM0MS4zIDBINTEydjUxMkgzNDEuM3oiLz4KICA8L2c+Cjwvc3ZnPgo=

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzUiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCAxMzUgMTAiPgogIDxwYXRoIGQ9Ik02Ny43ODIxMjUsMCBMNjQuMjc5OTY0OCw4LjAxMjcwMzEgTDc5LDguMDEyNzAzMSBMNzguMTIwMzU2MywxMCBMNTYsMTAgTDYwLjE0NjEzNzksMC40MzE5MDU0NjUgQzYwLjI1OTg2NzIsMC4xNjk1NzE2NCA2MC41MTY5MzA3LDAgNjAuODAwOTYwOSwwIEw2MC44MDA5NjA5LDAgTDY3Ljc4MjEyNSwwIFogTTQwLjc3ODIwNjQsMCBDNDAuNzc4MjA2NCwwIDM4LjA5MDEyNDQsNi4xODYwNDUxNCAzNy43MzQ1ODcxLDcuMDA3NTYyMzMgQzM3LjM3ODE4MTksNy44MjkwNzk1MiAzNy40Nzc5ODY5LDguMDE0MDAyMTMgMzguNzUzNzU1NSw4LjAxNDAwMjEzIEwzOC43NTM3NTU1LDguMDE0MDAyMTMgTDQ2LjY5NDE4NTMsOC4wMTIyMjk3MSBDNDcuOTIxMDYzOCw4LjAxNTE4Mzc0IDQ4LjIwODYxOCw3LjgwOTg3ODI5IDQ4LjU0Nzk1NTEsNy4wMzA2MDM4IEM0OC43MzcxNTA3LDYuNTk1NDc0NDIgNTAuNzYxODkwOSwxLjg4OTk5MTczIDUxLjM4OTkzOTMsMC40Mjk4MTIxMjMgQzUxLjUwMjQ3MzEsMC4xNjgzODAwMDcgNTEuNzU1NjAxOCwwLjAwMDI5NTQwMzUyMSA1Mi4wMzUzNDUxLDAuMDAwMjk1NDAzNTIxIEw1Mi4wMzUzNDUxLDAuMDAwMjk1NDAzNTIxIEw1OSwwIEM1OSwwIDU3Ljg2NzcxOTIsMi43MTM4NzIxNSA1Ni4zMzE1ODk4LDYuMzMwNDk3NDYgQzU0Ljc5NDg4MTcsOS45NDcxMjI3NyA1NC42NTM0MTg5LDEwIDQ5LjY1NjUxNDEsMTAgTDQ5LjY1NjUxNDEsMTAgTDMzLjk1Njc1LDEwIEMyOS45NDgwNTk2LDEwIDI5LjM2NjI5NzUsOS42NTY3NDExMSAzMC41MzMwMDM4LDYuODE0NjYzODMgTDMwLjUzMzAwMzgsNi44MTQ2NjM4MyBMMzMuMjY5Mzk3MiwwLjQzMTI4OTE0MSBDMzMuMzgxNjQxNiwwLjE2OTI2NjIxOCAzMy42MzUwNTk2LDAgMzMuOTE1MzgxNiwwIEwzMy45MTUzODE2LDAgWiBNMTM1LDAgTDEzNC4xNDY4MywxLjk4Nzg4Nzc0IEwxMTQuMzk5NDU4LDEuOTg3ODg3NzQgQzExNC4xMjQyMjMsMS45ODc4ODc3NCAxMTMuODYyNzIsMi4xMzQ3MTE5NiAxMTMuNzg0NDE2LDIuMzMzMjM0ODYgTDExMy43ODQ0MTYsMi4zMzMyMzQ4NiBMMTEzLjEwMzM0MSwzLjk3NjA3MDkgTDEzMS43NjYxMzYsMy45NzYwNzA5IEMxMzMuMTcyMTEzLDMuOTc2MDcwOSAxMzIuOTM1NzM4LDQuODczMjY0NCAxMzIuODQ2MDM5LDUuMDc0NDQ2MDkgTDEzMi44NDYwMzksNS4wNzQ0NDYwOSBMMTMxLjM5NTY1LDguNDM1NzQ1OTQgQzEzMS4xNTIyNjMsOC45MTM0NDE2NSAxMzAuNTMzNDIzLDkuOTk4MjI3NDcgMTI5LjA0NTA1MSw5Ljk5ODIyNzQ3IEwxMjkuMDQ1MDUxLDkuOTk4MjI3NDcgTDEwMywxMCBMMTAzLjg5NjQxMyw4LjAxMjcwMzEgTDEyMy41MjE2NTMsOC4wMTI3MDMxIEMxMjMuODE4MjE4LDguMDEyNzAzMSAxMjQuMTAwMTczLDcuODU0MzU3NDYgMTI0LjE4MzczNiw3LjY0MDQ3MjY3IEwxMjQuMTgzNzM2LDcuNjQwNDcyNjcgTDEyNC43Nzg5MSw2LjM3NzU0ODAxIEMxMjQuODY3MTQ5LDYuMTYwMTE4MTcgMTI0LjY3NDAxNyw1Ljk2MzM2NzggMTI0LjM3MjE5Myw1Ljk2MzM2NzggTDEyNC4zNzIxOTMsNS45NjMzNjc4IEwxMDYuMjc4ODYsNS45NjMzNjc4IEMxMDQuODk2ODQyLDUuOTYzMzY3OCAxMDUuMTE4MzE1LDUuMTE0OTE4NzYgMTA1LjM0MjcxMSw0LjYxODAyMDY4IEMxMDUuNTY1NjQ1LDQuMTIxMTIyNiAxMDYuNTM2NTY0LDEuNzA2OTQyMzkgMTA2Ljc3OTM2NywxLjIyODM2MDQxIEMxMDcuMDIyNDYxLDAuNzUwNjY0Njk3IDEwNy41NDQyOTcsMCAxMDkuMDMyNjcsMCBMMTA5LjAzMjY3LDAgTDEzNSwwIFogTTMyLDAgTDMwLjk2ODMwNzksMi4zODM2MTEwMSBDMzAuNzkzNDM1LDIuNzQ5MDI1MTcgMzAuMzk4OTQ3MywzLjEzNTQxMjk3IDI5Ljc2NDY2NzIsMy4yNzc1MDIwNyBMMjkuNzY0NjY3MiwzLjI3NzUwMjA3IEwyNi40MDY2OTY2LDMuOTc1ODM1OTkgTDI5LjgyMTEwNjEsNC45Njg5ODI2MyBDMjkuODU1OTA1Myw0Ljk4NzAwMjI1IDI5Ljg2ODE4NzMsNS4wMjk4MzU3NiAyOS44NTAzNDkxLDUuMDcxMTkyMjUgQzI5LjM4MDEyMjEsNi4xNjMyOTkwNyAyOC44NzA0MTcxLDcuMzQyODQ1MzMgMjguNzg2NzgyMiw3LjUyMTg1OTg2IEMyNy45NTQ4MTk2LDkuMzAzNDM4NSAyNy4zNzU4MDg4LDkuODAzODUyMDYgMjUuMzk5Mjc2Miw5Ljk0NDc1OTU0IEMyNC42NzE0MTg2LDkuOTk2NDU1MTYgMjMuNzU2MTEzNiwxMCAyMi41NjU5MjQ2LDEwIEMyMi41MDUwNjc1LDEwIDIyLjQ0MjQwMTIsOS45OTk5OTk5MSAyMi4zNzc5NzgxLDkuOTk5OTk5NzQgTDAsOS45OTk0MDkxOSBMMy41ODIyNjQxNCwxLjc1OTEyNzk3IEM0LjAzMTQzNjIsMC43MjU4MDY0NTIgNS4wODcxMDc1LDAgNi4xNDA3MzE4MSwwIEw2LjE0MDczMTgxLDAgTDMyLDAgWiBNOTEuNzgyMjY3OSwwIEw4OC4yODAxNTI0LDguMDEyNzAzMSBMMTAzLDguMDEyNzAzMSBMMTAyLjEyMDA3NCwxMCBMODAsMTAgTDg0LjE0NjM3ODEsMC40MzE5MDU0NjUgQzg0LjI1OTgxMjksMC4xNjk1NzE2NCA4NC41MTY4NzMxLDAgODQuODAwODk5NywwIEw4NC44MDA4OTk3LDAgTDkxLjc4MjI2NzksMCBaIE0yMS45MDAzNTQ2LDUuOTYzNjA2MjkgTDkuMjIyMDYzODIsNS45NjM2MDYyOSBMOC4zMjU0NzQyOCw4LjAxMjUyNTExIEwxOS41NTk3NDcsOC4wMTIyMjk3MSBDMjAuNzk5OTQxNSw4LjAxNTE4Mzc0IDIxLjA5MDkwOTEsNy44MDk1ODI4OSAyMS40MzM2MzY3LDcuMDMwMzA4NCBDMjEuNTMzMDYyOCw2LjgwNDYyMDExIDIxLjM1MzgwMzQsNy4yMTM0NTg1OCAyMS45MDAzNTQ2LDUuOTYzNjA2MjkgTDIxLjkwMDM1NDYsNS45NjM2MDYyOSBaIE0yMy4wMTEyOTUxLDEuOTg3NzcwMjkgTDExLjAxOTMzNjksMS45ODc3NzAyOSBDMTAuOTIyNTQyNywyLjIwODE0MTMyIDEwLjEyMDcwMDQsMy45NzU4MzU5OSAxMC4xMjA3MDA0LDMuOTc1ODM1OTkgTDEwLjEyMDcwMDQsMy45NzU4MzU5OSBMMjIuNzgwNTY4LDMuOTc1ODM1OTkgQzIyLjc4MDU2OCwzLjk3NTgzNTk5IDIzLjE3OTczNDYsMy4wOTExMDI0NSAyMy4zODM4NTA2LDIuNjM0NzA0MDEgQzIzLjUzMDM1NzksMi4zMDcxMDE1IDIzLjM0NjQxOTYsMS45ODgwNjU3IDIzLjAxMTI5NTEsMS45ODc3NzAyOSBMMjMuMDExMjk1MSwxLjk4Nzc3MDI5IFoiLz4KPC9zdmc+Cg==

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWNoIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2Utd2lkdGg9IjFwdCI+CiAgICA8cGF0aCBmaWxsPSJyZWQiIGQ9Ik0wIDBoNTEydjUxMkgweiIvPgogICAgPGcgZmlsbD0iI2ZmZiI+CiAgICAgIDxwYXRoIGQ9Ik05NiAyMDhoMzIwdjk2SDk2eiIvPgogICAgICA8cGF0aCBkPSJNMjA4IDk2aDk2djMyMGgtOTZ6Ii8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWRlIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPHBhdGggZmlsbD0iI2ZjMCIgZD0iTTAgMzQxLjNoNTEyVjUxMkgweiIvPgogIDxwYXRoIGZpbGw9IiMwMDAwMDEiIGQ9Ik0wIDBoNTEydjE3MC43SDB6Ii8+CiAgPHBhdGggZmlsbD0icmVkIiBkPSJNMCAxNzAuN2g1MTJ2MTcwLjZIMHoiLz4KPC9zdmc+Cg==

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWZyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMGg1MTJ2NTEySDB6Ii8+CiAgPHBhdGggZmlsbD0iIzAwMDA5MSIgZD0iTTAgMGgxNzAuN3Y1MTJIMHoiLz4KICA8cGF0aCBmaWxsPSIjZTEwMDBmIiBkPSJNMzQxLjMgMEg1MTJ2NTEySDM0MS4zeiIvPgo8L3N2Zz4K

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWdiIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPHBhdGggZmlsbD0iIzAxMjE2OSIgZD0iTTAgMGg1MTJ2NTEySDB6Ii8+CiAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTUxMiAwdjY0TDMyMiAyNTZsMTkwIDE4N3Y2OWgtNjdMMjU0IDMyNCA2OCA1MTJIMHYtNjhsMTg2LTE4N0wwIDc0VjBoNjJsMTkyIDE4OEw0NDAgMHoiLz4KICA8cGF0aCBmaWxsPSIjQzgxMDJFIiBkPSJtMTg0IDMyNCAxMSAzNEw0MiA1MTJIMHYtM3ptMTI0LTEyIDU0IDggMTUwIDE0N3Y0NXpNNTEyIDAgMzIwIDE5NmwtNC00NEw0NjYgMHpNMCAxbDE5MyAxODktNTktOEwwIDQ5eiIvPgogIDxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik0xNzYgMHY1MTJoMTYwVjB6TTAgMTc2djE2MGg1MTJWMTc2eiIvPgogIDxwYXRoIGZpbGw9IiNDODEwMkUiIGQ9Ik0wIDIwOHY5Nmg1MTJ2LTk2ek0yMDggMHY1MTJoOTZWMHoiLz4KPC9zdmc+Cg==

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMSIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDExIDIwIj4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMS4zNjYwOCw2LjQ1Mjg0IEwyMS4zNjYwOCw5LjQ5ODg0IEwxOS41NTQ0OCw5LjQ5ODg0IEMxOC44OTI4OCw5LjQ5ODg0IDE4LjQ0Njg4LDkuNjM3NjQgMTguMjE2MDgsOS45MTQ0NCBDMTcuOTg1MjgsMTAuMTkxMjQgMTcuODcwMDgsMTAuNjA2ODQgMTcuODcwMDgsMTEuMTYwNDQgTDE3Ljg3MDA4LDEzLjM0MTI0IEwyMS4yNTA4OCwxMy4zNDEyNCBMMjAuODAwODgsMTYuNzU2ODQgTDE3Ljg3MDA4LDE2Ljc1Njg0IEwxNy44NzAwOCwyNS41MTQ0NCBMMTQuMzM5MjgsMjUuNTE0NDQgTDE0LjMzOTI4LDE2Ljc1Njg0IEwxMS4zOTY4OCwxNi43NTY4NCBMMTEuMzk2ODgsMTMuMzQxMjQgTDE0LjMzOTI4LDEzLjM0MTI0IEwxNC4zMzkyOCwxMC44MjYwNCBDMTQuMzM5MjgsOS4zOTUyNCAxNC43MzkyOCw4LjI4NTY0IDE1LjUzOTI4LDcuNDk3MjQgQzE2LjMzOTI4LDYuNzA4NDQgMTcuNDA0NDgsNi4zMTQ0NCAxOC43MzUyOCw2LjMxNDQ0IEMxOS44NjYwOCw2LjMxNDQ0IDIwLjc0Mjg4LDYuMzYwNDQgMjEuMzY2MDgsNi40NTI4NCBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTEgLTYpIi8+Cjwvc3ZnPgo=

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMCAwIDIwIDE0Ij4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMy44ODM4MzY3LDE4LjU1OTE4MzcgTDE5LjE3NTI2NTMsMTUuODI2MTIyNCBMMTMuODgzODM2NywxMy4wNjA0MDgyIEwxMy44ODM4MzY3LDE4LjU1OTE4MzcgWiBNMTUuOTA2Mjg1Nyw5LjEzNTUxMDIgQzE3LjEzMDc3NTUsOS4xMzU1MTAyIDE4LjMxMzIyNDUsOS4xNTE4MzY3MyAxOS40NTQwNDA4LDkuMTg0ODk3OTYgQzIwLjU5NDQ0OSw5LjIxNzU1MTAyIDIxLjQzMDc3NTUsOS4yNTIyNDQ5IDIxLjk2MzAyMDQsOS4yODg1NzE0MyBMMjIuNzYwOTc5Niw5LjMzMjI0NDkgQzIyLjc2ODMyNjUsOS4zMzIyNDQ5IDIyLjgyOTk1OTIsOS4zMzc5NTkxOCAyMi45NDY2OTM5LDkuMzQ4NTcxNDMgQzIzLjA2MzQyODYsOS4zNTk1OTE4NCAyMy4xNDcxMDIsOS4zNzA2MTIyNCAyMy4xOTgxMjI0LDkuMzgxNjMyNjUgQzIzLjI0OTE0MjksOS4zOTIyNDQ5IDIzLjMzNDg1NzEsOS40MDg5Nzk1OSAyMy40NTUyNjUzLDkuNDMwNjEyMjQgQzIzLjU3NTI2NTMsOS40NTI2NTMwNiAyMy42NzkzNDY5LDkuNDgxNjMyNjUgMjMuNzY2NjkzOSw5LjUxNzk1OTE4IEMyMy44NTQwNDA4LDkuNTU0NjkzODggMjMuOTU2MDgxNiw5LjYwMjA0MDgyIDI0LjA3MjgxNjMsOS42NjA0MDgxNiBDMjQuMTg5NTUxLDkuNzE4MzY3MzUgMjQuMzAyMjA0MSw5Ljc4OTc5NTkyIDI0LjQxMTU5MTgsOS44NzM0NjkzOSBDMjQuNTIwOTc5Niw5Ljk1NzE0Mjg2IDI0LjYyNjY5MzksMTAuMDUzODc3NiAyNC43Mjg3MzQ3LDEwLjE2MzI2NTMgQzI0Ljc3MjQwODIsMTAuMjA2OTM4OCAyNC44Mjg3MzQ3LDEwLjI3NDI4NTcgMjQuODk4MTIyNCwxMC4zNjUzMDYxIEMyNC45Njc1MTAyLDEwLjQ1NjMyNjUgMjUuMDczMjI0NSwxMC42Njk3OTU5IDI1LjIxNTI2NTMsMTEuMDA0ODk4IEMyNS4zNTczMDYxLDExLjM0IDI1LjQ1NDA0MDgsMTEuNzA4MTYzMyAyNS41MDUwNjEyLDEyLjEwODk3OTYgQzI1LjU2MzAyMDQsMTIuNTc1NTEwMiAyNS42MDg3MzQ3LDEzLjA3MzA2MTIgMjUuNjQxMzg3OCwxMy42MDEyMjQ1IEMyNS42NzQ0NDksMTQuMTI5Nzk1OSAyNS42OTQ0NDksMTQuNTQzMjY1MyAyNS43MDE3OTU5LDE0Ljg0MjA0MDggTDI1LjcwMTc5NTksMTYuNzY2MTIyNCBDMjUuNzA5MTQyOSwxNy44MjMyNjUzIDI1LjY0MzQyODYsMTguODggMjUuNTA1MDYxMiwxOS45MzY3MzQ3IEMyNS40NTQwNDA4LDIwLjMzNzU1MSAyNS4zNjI2MTIyLDIwLjcgMjUuMjMxNTkxOCwyMS4wMjQ0ODk4IEMyNS4xMDA1NzE0LDIxLjM0ODU3MTQgMjQuOTgzODM2NywyMS41NzMwNjEyIDI0Ljg4MTc5NTksMjEuNjk2NzM0NyBMMjQuNzI4NzM0NywyMS44ODI0NDkgQzI0LjYyNjY5MzksMjEuOTkxODM2NyAyNC41MjA5Nzk2LDIyLjA4ODU3MTQgMjQuNDExNTkxOCwyMi4xNzIyNDQ5IEMyNC4zMDIyMDQxLDIyLjI1NjMyNjUgMjQuMTg5NTUxLDIyLjMyNTMwNjEgMjQuMDcyODE2MywyMi4zOCBDMjMuOTU2MDgxNiwyMi40MzQ2OTM5IDIzLjg1NDA0MDgsMjIuNDgwNDA4MiAyMy43NjY2OTM5LDIyLjUxNjczNDcgQzIzLjY3OTM0NjksMjIuNTUzMDYxMiAyMy41NzUyNjUzLDIyLjU4MjQ0OSAyMy40NTUyNjUzLDIyLjYwNDA4MTYgQzIzLjMzNDg1NzEsMjIuNjI2MTIyNCAyMy4yNDc1MTAyLDIyLjY0MjQ0OSAyMy4xOTI4MTYzLDIyLjY1MzQ2OTQgQzIzLjEzODEyMjQsMjIuNjY0MDgxNiAyMy4wNTQwNDA4LDIyLjY3NTEwMiAyMi45NDEzODc4LDIyLjY4NjEyMjQgQzIyLjgyODMyNjUsMjIuNjk3MTQyOSAyMi43NjgzMjY1LDIyLjcwMjQ0OSAyMi43NjA5Nzk2LDIyLjcwMjQ0OSBDMjAuOTMxNTkxOCwyMi44NDEyMjQ1IDE4LjY0NjY5MzksMjIuOTEwMjA0MSAxNS45MDYyODU3LDIyLjkxMDIwNDEgQzE0LjM5NzcxNDMsMjIuODk1NTEwMiAxMy4wODc1MTAyLDIyLjg3MTgzNjcgMTEuOTc2MDgxNiwyMi44MzkxODM3IEMxMC44NjQ2NTMxLDIyLjgwNjUzMDYgMTAuMTM0MDQwOCwyMi43NzkxODM3IDkuNzg0MjQ0OSwyMi43NTcxNDI5IEw5LjI0ODMyNjUzLDIyLjcxMzQ2OTQgTDguODU0ODU3MTQsMjIuNjY5Nzk1OSBDOC41OTI0MDgxNiwyMi42MzM0Njk0IDguMzk0MDQwODIsMjIuNTk2NzM0NyA4LjI1OTM0Njk0LDIyLjU2MDQwODIgQzguMTI0MjQ0OSwyMi41MjQwODE2IDcuOTM4NTMwNjEsMjIuNDQ3MzQ2OSA3LjcwMTc5NTkyLDIyLjMzMTAyMDQgQzcuNDY0NjUzMDYsMjIuMjE0Mjg1NyA3LjI1ODkzODc4LDIyLjA2NDg5OCA3LjA4MzgzNjczLDIxLjg4MjQ0OSBDNy4wNDAxNjMyNywyMS44Mzg3NzU1IDYuOTgzODM2NzMsMjEuNzcxNDI4NiA2LjkxNDQ0ODk4LDIxLjY4MDQwODIgQzYuODQ1MDYxMjIsMjEuNTg5Mzg3OCA2LjczOTc1NTEsMjEuMzc1OTE4NCA2LjU5NzMwNjEyLDIxLjA0MDgxNjMgQzYuNDU1MjY1MzEsMjAuNzA1NzE0MyA2LjM1ODUzMDYxLDIwLjMzNzU1MSA2LjMwNzkxODM3LDE5LjkzNjczNDcgQzYuMjQ5NTUxMDIsMTkuNDcwMjA0MSA2LjIwMzgzNjczLDE4Ljk3MjY1MzEgNi4xNzExODM2NywxOC40NDQ0ODk4IEM2LjEzODEyMjQ1LDE3LjkxNTkxODQgNi4xMTgxMjI0NSwxNy41MDI0NDkgNi4xMTA3NzU1MSwxNy4yMDM2NzM1IEw2LjExMDc3NTUxLDE1LjI3OTU5MTggQzYuMTAzODM2NzMsMTQuMjIyODU3MSA2LjE2OTE0Mjg2LDEzLjE2NTcxNDMgNi4zMDc5MTgzNywxMi4xMDg5Nzk2IEM2LjM1ODUzMDYxLDExLjcwODE2MzMgNi40NDk5NTkxOCwxMS4zNDU3MTQzIDYuNTgwOTc5NTksMTEuMDIxMjI0NSBDNi43MTI0MDgxNiwxMC42OTcxNDI5IDYuODI4NzM0NjksMTAuNDczMDYxMiA2LjkzMDc3NTUxLDEwLjM0ODk3OTYgTDcuMDgzODM2NzMsMTAuMTYzMjY1MyBDNy4xODU4Nzc1NSwxMC4wNTM4Nzc2IDcuMjkxNTkxODQsOS45NTcxNDI4NiA3LjQwMDk3OTU5LDkuODczNDY5MzkgQzcuNTEwMzY3MzUsOS43ODk3OTU5MiA3LjYyMzQyODU3LDkuNzE4MzY3MzUgNy43Mzk3NTUxLDkuNjYwNDA4MTYgQzcuODU2NDg5OCw5LjYwMjA0MDgyIDcuOTU4NTMwNjEsOS41NTQ2OTM4OCA4LjA0NTg3NzU1LDkuNTE3OTU5MTggQzguMTMzMjI0NDksOS40ODE2MzI2NSA4LjIzNzMwNjEyLDkuNDUyNjUzMDYgOC4zNTc3MTQyOSw5LjQzMDYxMjI0IEM4LjQ3NzcxNDI5LDkuNDA4OTc5NTkgOC41NjM0Mjg1Nyw5LjM5MjI0NDkgOC42MTQ0NDg5OCw5LjM4MTYzMjY1IEM4LjY2NTQ2OTM5LDkuMzcwNjEyMjQgOC43NDkxNDI4Niw5LjM1OTU5MTg0IDguODY1ODc3NTUsOS4zNDg1NzE0MyBDOC45ODI2MTIyNCw5LjMzNzk1OTE4IDkuMDQ0MjQ0OSw5LjMzMjI0NDkgOS4wNTE1OTE4NCw5LjMzMjI0NDkgQzEwLjg4MDk3OTYsOS4yMDEyMjQ0OSAxMy4xNjU4Nzc2LDkuMTM1NTEwMiAxNS45MDYyODU3LDkuMTM1NTEwMiBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNiAtOSkiLz4KPC9zdmc+Cg==

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWl0IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2Utd2lkdGg9IjFwdCI+CiAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDkyNDYiIGQ9Ik0wIDBoMTcwLjd2NTEySDB6Ii8+CiAgICA8cGF0aCBmaWxsPSIjY2UyYjM3IiBkPSJNMzQxLjMgMEg1MTJ2NTEySDM0MS4zeiIvPgogIDwvZz4KPC9zdmc+Cg==

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWx0IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2Utd2lkdGg9IjFwdCIgdHJhbnNmb3JtPSJzY2FsZSguNTEzMTQgMS4wMzIyKSI+CiAgICA8cmVjdCB3aWR0aD0iMTA2MyIgaGVpZ2h0PSI3MDguNyIgZmlsbD0iIzAwNmE0NCIgcng9IjAiIHJ5PSIwIiB0cmFuc2Zvcm09InNjYWxlKC45Mzg2NSAuNjk2ODYpIi8+CiAgICA8cmVjdCB3aWR0aD0iMTA2MyIgaGVpZ2h0PSIyMzYuMiIgeT0iNDc1LjYiIGZpbGw9IiNjMTI3MmQiIHJ4PSIwIiByeT0iMCIgdHJhbnNmb3JtPSJzY2FsZSguOTM4NjUgLjY5Njg2KSIvPgogICAgPHBhdGggZmlsbD0iI2ZkYjkxMyIgZD0iTTAgMGg5OTcuOHYxNjQuNkgweiIvPgogIDwvZz4KPC9zdmc+Cg==

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLW5sIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPHBhdGggZmlsbD0iI2FlMWMyOCIgZD0iTTAgMGg1MTJ2MTcwLjdIMHoiLz4KICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAxNzAuN2g1MTJ2MTcwLjZIMHoiLz4KICA8cGF0aCBmaWxsPSIjMjE0NjhiIiBkPSJNMCAzNDEuM2g1MTJWNTEySDB6Ii8+Cjwvc3ZnPgo=

View File

@@ -0,0 +1 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLXBsIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgPGcgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MTIgNTEySDBWMGg1MTJ6Ii8+CiAgICA8cGF0aCBmaWxsPSIjZGMxNDNjIiBkPSJNNTEyIDUxMkgwVjI1Nmg1MTJ6Ii8+CiAgPC9nPgo8L3N2Zz4K

1232
public/index.html Normal file

File diff suppressed because it is too large Load Diff

24
src/App.js Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import Header from './components/Header';
import Hero from './components/Hero';
import TopEBikes from './components/TopEBikes';
import ProductSections from './components/ProductSections';
import Footer from './components/Footer';
import CookieConsent from './components/CookieConsent';
function App() {
return (
<div className="App">
<Header />
<main>
<Hero />
<TopEBikes />
<ProductSections />
</main>
<Footer />
<CookieConsent />
</div>
);
}
export default App;

View File

@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { X } from 'lucide-react';
const CookieConsent = () => {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-lg">
<div className="container-max py-4">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0">
<div className="flex-1 pr-4">
<h3 className="font-bold text-bulls-gray mb-2">Privacy Settings</h3>
<p className="text-sm text-gray-600 leading-relaxed">
We use Cookies to offer a variety of services, to continuously improve them and to display advertisements based on your interests on our website, social media and partner websites. By clicking on "OK" you consent to the use of Cookies. You can view, change or revoke your Cookie settings at any time via the privacy policy page. You can find out more in the privacy policy.
</p>
<div className="flex space-x-2 mt-2">
<a href="#" className="text-xs text-bulls-blue hover:underline">Privacy Policy</a>
<a href="#" className="text-xs text-bulls-blue hover:underline">Imprint</a>
</div>
</div>
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm border border-gray-300 rounded hover:bg-gray-50 transition-colors"
>
More
</button>
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm border border-gray-300 rounded hover:bg-gray-50 transition-colors"
>
Deny
</button>
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm bg-bulls-gray text-white rounded hover:bg-gray-800 transition-colors"
>
Accept All
</button>
</div>
</div>
</div>
</div>
);
};
export default CookieConsent;

84
src/components/Footer.js Normal file
View File

@@ -0,0 +1,84 @@
import React from 'react';
import { ArrowRight } from 'lucide-react';
const Footer = () => {
return (
<footer className="bg-white">
{/* Newsletter section */}
<div className="border-t border-gray-200 py-12">
<div className="container-max text-center">
<h3 className="text-2xl font-bold text-bulls-gray mb-4">
STAY UPDATED WITH OUR LATEST NEWS
</h3>
<p className="text-gray-600 mb-8">
Subscribe to our newsletter and be the first to know about new products and updates
</p>
<div className="flex justify-center">
<button className="btn-primary inline-flex items-center space-x-2">
<span>FIND YOUR DEALER</span>
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
{/* Main footer */}
<div className="bg-bulls-gray text-white py-12">
<div className="container-max">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Company info */}
<div>
<div className="text-2xl font-bold mb-4">
<span className="bg-white text-black px-2 py-1 mr-1 transform -skew-x-12">BULLS</span>
</div>
<p className="text-gray-300 text-sm leading-relaxed">
Premium E-Bikes and Mountain Bikes for every adventure. Experience the future of cycling with BULLS.
</p>
</div>
{/* Quick links */}
<div>
<h4 className="font-bold mb-4">QUICK LINKS</h4>
<ul className="space-y-2 text-sm">
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">E-Bikes</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Mountain Bikes</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Service</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Dealer Search</a></li>
</ul>
</div>
{/* Support */}
<div>
<h4 className="font-bold mb-4">SUPPORT</h4>
<ul className="space-y-2 text-sm">
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Contact</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">FAQ</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Warranty</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Manual Downloads</a></li>
</ul>
</div>
{/* Legal */}
<div>
<h4 className="font-bold mb-4">LEGAL</h4>
<ul className="space-y-2 text-sm">
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Imprint</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Data Privacy Notice</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Cookies</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors">Terms</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-600 mt-8 pt-8 text-center">
<p className="text-gray-400 text-sm">
© 2024 BULLS Bikes. All rights reserved. | Prices include VAT and are subject to change.
</p>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

83
src/components/Header.js Normal file
View File

@@ -0,0 +1,83 @@
import React, { useState } from 'react';
import { Search, Menu, X, MapPin, User } from 'lucide-react';
const Header = () => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-black bg-opacity-90 backdrop-blur-sm">
{/* Top bar */}
<div className="bg-gray-800 text-white text-sm py-2">
<div className="container-max flex justify-between items-center">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<MapPin className="w-4 h-4" />
<span>Dealer search</span>
</div>
<div className="flex items-center space-x-1">
<User className="w-4 h-4" />
<span>About us</span>
</div>
</div>
<div className="hidden md:block">
<span>Premium E-Bikes & Mountain Bikes</span>
</div>
</div>
</div>
{/* Main navigation */}
<nav className="py-4">
<div className="container-max flex items-center justify-between">
{/* Logo */}
<div className="flex items-center">
<div className="text-white text-2xl font-bold">
<span className="bg-white text-black px-2 py-1 mr-1 transform -skew-x-12">BULLS</span>
</div>
</div>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center space-x-8">
<div className="nav-link cursor-pointer">E-BIKES</div>
<div className="nav-link cursor-pointer">BIKES</div>
<div className="nav-link cursor-pointer">SERVICE</div>
</div>
{/* Right side icons */}
<div className="flex items-center space-x-4">
<Search className="w-6 h-6 text-white cursor-pointer hover:text-bulls-blue transition-colors" />
<div className="w-6 h-6 text-white cursor-pointer hover:text-bulls-blue transition-colors">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
{/* Mobile menu button */}
<button
className="lg:hidden text-white"
onClick={toggleMobileMenu}
>
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile Navigation */}
{isMobileMenuOpen && (
<div className="lg:hidden bg-black bg-opacity-95 mt-4">
<div className="container-max py-4 space-y-4">
<div className="nav-link cursor-pointer block py-2">E-BIKES</div>
<div className="nav-link cursor-pointer block py-2">BIKES</div>
<div className="nav-link cursor-pointer block py-2">SERVICE</div>
</div>
</div>
)}
</nav>
</header>
);
};
export default Header;

78
src/components/Hero.js Normal file
View File

@@ -0,0 +1,78 @@
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
const fadeUpPreset = (delay = 0, duration = 1.2) => ({
initial: { opacity: 0, y: 30 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.2 },
transition: { delay, duration, ease: "easeOut" }
});
const Hero = () => {
const shouldReduceMotion = useReducedMotion();
if (shouldReduceMotion) {
return (
<section className="relative h-screen flex items-center justify-start" style={{
backgroundImage: "url('https://www.bulls-bikes.com/fileadmin/_processed_/2/8/csm_BULLS_Grinder_3_Lifestyle_2024_01_b5c8a5e5a7.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}>
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
<div className="relative z-10 container-max">
<div className="max-w-2xl">
<h1 className="hero-text text-hero-sm md:text-hero mb-4">
READY FOR 2026
</h1>
<h2 className="hero-text text-2xl md:text-4xl mb-6">
ONE OF OUR HIGHLIGHTS:
</h2>
<h3 className="hero-text text-4xl md:text-6xl font-black">
THE GRINDER 3
</h3>
</div>
</div>
</section>
);
}
return (
<motion.section
{...fadeUpPreset(0.1, 0.8)}
className="relative h-screen flex items-center justify-start"
style={{
backgroundImage: "url('https://www.bulls-bikes.com/fileadmin/_processed_/2/8/csm_BULLS_Grinder_3_Lifestyle_2024_01_b5c8a5e5a7.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
>
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
<div className="relative z-10 container-max">
<div className="max-w-2xl">
<motion.h1
{...fadeUpPreset(0.2, 0.8)}
className="hero-text text-hero-sm md:text-hero mb-4"
>
READY FOR 2026
</motion.h1>
<motion.h2
{...fadeUpPreset(0.4, 0.8)}
className="hero-text text-2xl md:text-4xl mb-6"
>
ONE OF OUR HIGHLIGHTS:
</motion.h2>
<motion.h3
{...fadeUpPreset(0.6, 0.8)}
className="hero-text text-4xl md:text-6xl font-black"
>
THE GRINDER 3
</motion.h3>
</div>
</div>
</motion.section>
);
};
export default Hero;

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
const fadeUpPreset = (delay = 0, duration = 1.2) => ({
initial: { opacity: 0, y: 30 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.2 },
transition: { delay, duration, ease: "easeOut" }
});
const ProductSections = () => {
const shouldReduceMotion = useReducedMotion();
const sections = [
{
title: "VUCA EVO | pinion",
subtitle: "MAXIMUM PERFORMANCE WITH A MINIMUM OF MAINTENANCE",
buttonText: "SEE ALL MODELS",
image: "https://www.bulls-bikes.com/fileadmin/_processed_/9/3/csm_BULLS_VUCA_EVO_Pinion_Lifestyle_2024_01_b5c8a5e5a7.jpg",
textColor: "text-white",
buttonStyle: "btn-secondary"
},
{
title: "NO LIMITS!",
subtitle: "",
buttonText: "CHOOSE YOUR BIKE",
image: "https://www.bulls-bikes.com/fileadmin/_processed_/7/4/csm_BULLS_Lifestyle_MTB_2024_02_c8f5e5a7b5.jpg",
textColor: "text-white",
buttonStyle: "btn-secondary"
},
{
title: "MAXIMUM OFFROAD POWER",
subtitle: "",
buttonText: "CHOOSE YOUR E-BIKE",
image: "https://www.bulls-bikes.com/fileadmin/_processed_/5/6/csm_BULLS_Lifestyle_E-MTB_2024_03_a8c8f5e5a7.jpg",
textColor: "text-white",
buttonStyle: "btn-secondary"
}
];
const ProductSection = ({ section, index }) => {
const SectionComponent = shouldReduceMotion ? 'div' : motion.div;
const sectionProps = shouldReduceMotion ? {} : fadeUpPreset(index * 0.1, 1.0);
return (
<SectionComponent
{...sectionProps}
className="relative h-96 md:h-[500px] flex items-center justify-center overflow-hidden"
style={{
backgroundImage: `url('${section.image}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
>
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
<div className="relative z-10 text-center px-4">
<h2 className={`text-4xl md:text-6xl font-black mb-4 ${section.textColor}`}>
{section.title}
</h2>
{section.subtitle && (
<p className={`text-lg md:text-xl mb-8 max-w-2xl ${section.textColor}`}>
{section.subtitle}
</p>
)}
<button className={`${section.buttonStyle} inline-flex items-center space-x-2`}>
<span>{section.buttonText}</span>
<ArrowRight className="w-5 h-5" />
</button>
</div>
</SectionComponent>
);
};
return (
<div className="space-y-0">
{sections.map((section, index) => (
<ProductSection key={index} section={section} index={index} />
))}
</div>
);
};
export default ProductSections;

112
src/components/TopEBikes.js Normal file
View File

@@ -0,0 +1,112 @@
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import { Star } from 'lucide-react';
const fadeUpPreset = (delay = 0, duration = 1.2) => ({
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.2 },
transition: { delay, duration, ease: "easeOut" }
});
const TopEBikes = () => {
const shouldReduceMotion = useReducedMotion();
const bikes = [
{
name: "COPPERHEAD EVO 1",
price: "from 3,199.00*",
rating: 5,
image: "https://www.bulls-bikes.com/fileadmin/_processed_/4/1/csm_BULLS_Copperhead_EVO_1_29_black_2024_01_a8c8f5e5a7.jpg",
colors: ['#000', '#666', '#999']
},
{
name: "EVO CX 1",
price: "4,799.00*",
rating: 5,
image: "https://www.bulls-bikes.com/fileadmin/_processed_/8/2/csm_BULLS_EVO_CX_1_black_2024_01_b5c8a5e5a7.jpg",
colors: ['#000', '#333']
},
{
name: "GRINDER 1",
price: "3,499.00*",
rating: 5,
image: "https://www.bulls-bikes.com/fileadmin/_processed_/1/5/csm_BULLS_Grinder_1_black_2024_01_c8f5e5a7b5.jpg",
colors: ['#000', '#666', '#999', '#ccc']
}
];
const BikeCard = ({ bike, index }) => {
const CardComponent = shouldReduceMotion ? 'div' : motion.div;
const cardProps = shouldReduceMotion ? {} : fadeUpPreset(index * 0.1, 0.8);
return (
<CardComponent {...cardProps} className="product-card">
<div className="aspect-w-16 aspect-h-12 bg-gray-100">
<img
src={bike.image}
alt={bike.name}
className="w-full h-64 object-contain p-4"
/>
</div>
<div className="p-6">
<h3 className="text-lg font-bold text-bulls-gray mb-2">{bike.name}</h3>
<div className="flex items-center mb-3">
{[...Array(bike.rating)].map((_, i) => (
<Star key={i} className="w-4 h-4 text-yellow-400 fill-current" />
))}
</div>
<p className="text-xl font-bold text-bulls-gray mb-4">{bike.price}</p>
<div className="flex space-x-2 mb-4">
{bike.colors.map((color, i) => (
<div
key={i}
className="w-4 h-4 rounded-full border border-gray-300"
style={{ backgroundColor: color }}
></div>
))}
</div>
</div>
</CardComponent>
);
};
const SectionComponent = shouldReduceMotion ? 'section' : motion.section;
const sectionProps = shouldReduceMotion ? {} : fadeUpPreset(0.05, 1.0);
return (
<SectionComponent {...sectionProps} className="section-padding bg-bulls-light-gray">
<div className="container-max">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-bulls-gray mb-4">TOP E-BIKES</h2>
<div className="flex justify-center space-x-2 mb-8">
<div className="w-2 h-2 bg-bulls-blue rounded-full"></div>
<div className="w-2 h-2 bg-gray-300 rounded-full"></div>
<div className="w-2 h-2 bg-gray-300 rounded-full"></div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{bikes.map((bike, index) => (
<BikeCard key={index} bike={bike} index={index} />
))}
</div>
<div className="text-center mt-12">
<div className="flex justify-center space-x-1">
{[...Array(12)].map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${
i < 4 ? 'bg-bulls-blue' : 'bg-gray-300'
}`}
></div>
))}
</div>
</div>
</div>
</SectionComponent>
);
};
export default TopEBikes;

47
src/index.css Normal file
View File

@@ -0,0 +1,47 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer components {
.btn-primary {
@apply bg-bulls-blue text-white px-6 py-3 rounded-md font-medium hover:bg-blue-600 transition-colors duration-200;
}
.btn-secondary {
@apply bg-white text-bulls-gray border border-gray-300 px-6 py-3 rounded-md font-medium hover:bg-gray-50 transition-colors duration-200;
}
.section-padding {
@apply py-16 px-4 sm:px-6 lg:px-8;
}
.container-max {
@apply max-w-7xl mx-auto;
}
.hero-text {
@apply text-white font-bold tracking-wide;
}
.product-card {
@apply bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300;
}
.nav-link {
@apply text-white hover:text-bulls-blue transition-colors duration-200 font-medium;
}
}

13
src/index.js Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

25
tailwind.config.js Normal file
View File

@@ -0,0 +1,25 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
'bulls-gray': '#2a2a2a',
'bulls-light-gray': '#f5f5f5',
'bulls-blue': '#00a8e6',
'bulls-dark': '#1a1a1a',
'bulls-accent': '#ff6b35'
},
fontFamily: {
'sans': ['Arial', 'Helvetica', 'sans-serif']
},
fontSize: {
'hero': ['4rem', { lineHeight: '1.1' }],
'hero-sm': ['2.5rem', { lineHeight: '1.2' }]
}
},
},
plugins: [],
}

5
vercel.json Normal file
View File

@@ -0,0 +1,5 @@
{
"installCommand": "npm install",
"buildCommand": "CI=false npm run build",
"outputDirectory": "build"
}