Initial commit

This commit is contained in:
dk
2025-12-29 13:23:02 +02:00
commit 70792b5aca
60 changed files with 2005 additions and 0 deletions

1
.env.production Normal file
View File

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

View File

@@ -0,0 +1,83 @@
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: Cache node_modules
id: cache-node-modules
uses: actions/cache@v3
continue-on-error: true
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
- name: Install dependencies
run: |
set -euo pipefail
if [ -d node_modules ] && [ "${{ steps.cache-node-modules.outputs.cache-hit }}" == "true" ]; then
echo "Cache hit, verifying dependencies..."
npm ci --prefer-offline --no-audit --silent 2>&1 | tee install.log || npm install --no-audit --silent 2>&1 | tee install.log
else
echo "Cache miss, installing dependencies..."
npm ci --prefer-offline --no-audit --silent 2>&1 | tee install.log
fi
- name: Build (react-scripts build)
env:
CI: 'false'
run: |
set -euo pipefail
npm run build 2>&1 | tee build.log
timeout-minutes: 5
- name: Save node_modules cache
if: always()
uses: actions/cache@v3
continue-on-error: true
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
- name: Verify build folder exists
run: test -d build || (echo "No build folder. Check build logs above."; exit 1)
- name: Upload logs (always)
if: always()
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,49 @@
import React, { useState } from 'react';
import { Settings } from 'lucide-react';
const CookieConsent = () => {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg z-50">
<div className="container mx-auto px-4 py-4">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between space-y-4 lg:space-y-0">
<div className="flex items-start space-x-3">
<Settings className="w-6 h-6 text-gray-600 mt-1 flex-shrink-0" />
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-1">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>
</div>
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3 w-full lg:w-auto">
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200"
>
More
</button>
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200"
>
Deny
</button>
<button
onClick={() => setIsVisible(false)}
className="px-6 py-2 text-sm font-medium text-white bg-bulls-blue hover:bg-blue-600 rounded transition-colors duration-200"
>
Accept All
</button>
</div>
</div>
</div>
</div>
);
};
export default CookieConsent;

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

@@ -0,0 +1,46 @@
import React from 'react';
import { Facebook, Instagram, Youtube } from 'lucide-react';
const Footer = () => {
return (
<footer className="bg-bulls-light-gray py-12">
<div className="container mx-auto px-4">
<div className="text-center mb-8">
<p className="text-sm text-gray-600 mb-4">
ONLY DEALERS, NEVER ONLINE STORES | FIND YOUR DEALER
</p>
<div className="flex justify-center space-x-6">
<Facebook className="w-6 h-6 text-gray-600 hover:text-bulls-blue cursor-pointer" />
<Instagram className="w-6 h-6 text-gray-600 hover:text-bulls-blue cursor-pointer" />
<Youtube className="w-6 h-6 text-gray-600 hover:text-bulls-blue cursor-pointer" />
</div>
</div>
<div className="border-t border-gray-300 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<div className="flex flex-wrap justify-center md:justify-start space-x-6 text-sm">
<a href="#" className="text-gray-600 hover:text-bulls-blue">IMPRINT</a>
<a href="#" className="text-gray-600 hover:text-bulls-blue">DATA PRIVACY NOTICE</a>
<a href="#" className="text-gray-600 hover:text-bulls-blue">COOKIES</a>
</div>
<div className="flex items-center space-x-4">
<div className="w-8 h-6 bg-gray-300 rounded"></div>
<span className="text-sm text-gray-600">Deutschland</span>
</div>
</div>
<div className="mt-8 text-center text-xs text-gray-500">
<p className="mb-2">
* Prices are non-binding recommended retail prices including VAT.
</p>
<p>
Actual prices may vary and are determined by the respective BULLS dealer. Subject to technical changes, errors and omissions.
</p>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

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

@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { Search, Menu, X, MapPin, User } from 'lucide-react';
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm border-b border-gray-200">
{/* Top Bar */}
<div className="bg-gray-100 py-2">
<div className="container mx-auto px-4 flex justify-between items-center text-sm">
<div className="flex items-center space-x-4">
<button className="flex items-center space-x-1 text-gray-600 hover:text-bulls-blue">
<MapPin className="w-4 h-4" />
<span>Dealer search</span>
</button>
<button className="flex items-center space-x-1 text-gray-600 hover:text-bulls-blue">
<User className="w-4 h-4" />
<span>About us</span>
</button>
</div>
<div className="flex items-center space-x-4">
<Search className="w-4 h-4 text-gray-600 hover:text-bulls-blue cursor-pointer" />
<div className="w-6 h-6 bg-gray-300 rounded"></div>
</div>
</div>
</div>
{/* Main Navigation */}
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<div className="flex items-center">
<div className="w-16 h-8 bg-bulls-dark transform skew-x-12 flex items-center justify-center">
<span className="text-white font-bold text-lg transform -skew-x-12">BULLS</span>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-8">
<div className="relative group">
<button className="flex items-center space-x-1 text-gray-700 hover:text-bulls-blue font-medium">
<span>E-BIKES</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<div className="relative group">
<button className="flex items-center space-x-1 text-gray-700 hover:text-bulls-blue font-medium">
<span>BIKES</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<div className="relative group">
<button className="flex items-center space-x-1 text-gray-700 hover:text-bulls-blue font-medium">
<span>SERVICE</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</nav>
{/* Mobile Menu Button */}
<button
className="md:hidden p-2"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="md:hidden bg-white border-t border-gray-200">
<nav className="container mx-auto px-4 py-4 space-y-4">
<a href="#" className="block text-gray-700 hover:text-bulls-blue font-medium">E-BIKES</a>
<a href="#" className="block text-gray-700 hover:text-bulls-blue font-medium">BIKES</a>
<a href="#" className="block text-gray-700 hover:text-bulls-blue font-medium">SERVICE</a>
</nav>
</div>
)}
</header>
);
};
export default Header;

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

@@ -0,0 +1,70 @@
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
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 Hero = () => {
const shouldReduce = useReducedMotion();
if (shouldReduce) {
return (
<section className="relative h-screen flex items-center justify-start bg-cover bg-center" style={{
backgroundImage: "linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('https://www.bulls-bikes.com/media/image/bulls-grinder-3-hero-2026.jpg')"
}}>
<div className="container mx-auto px-4 pt-24">
<div className="max-w-2xl">
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold text-white mb-4 text-shadow">
READY FOR 2026
</h1>
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold text-white mb-6 text-shadow">
ONE OF OUR HIGHLIGHTS:
</h2>
<h3 className="text-6xl md:text-7xl lg:text-8xl font-bold text-white text-shadow">
THE GRINDER 3
</h3>
</div>
</div>
</section>
);
}
return (
<motion.section
{...fadeUpPreset(0.1, 1.0)}
className="relative h-screen flex items-center justify-start bg-cover bg-center"
style={{
backgroundImage: "linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('https://www.bulls-bikes.com/media/image/bulls-grinder-3-hero-2026.jpg')"
}}
>
<div className="container mx-auto px-4 pt-24">
<div className="max-w-2xl">
<motion.h1
{...fadeUpPreset(0.2, 1.0)}
className="text-5xl md:text-6xl lg:text-7xl font-bold text-white mb-4 text-shadow"
>
READY FOR 2026
</motion.h1>
<motion.h2
{...fadeUpPreset(0.4, 1.0)}
className="text-2xl md:text-3xl lg:text-4xl font-bold text-white mb-6 text-shadow"
>
ONE OF OUR HIGHLIGHTS:
</motion.h2>
<motion.h3
{...fadeUpPreset(0.6, 1.0)}
className="text-6xl md:text-7xl lg:text-8xl font-bold text-white text-shadow"
>
THE GRINDER 3
</motion.h3>
</div>
</div>
</motion.section>
);
};
export default Hero;

View File

@@ -0,0 +1,98 @@
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: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.2 },
transition: { delay, duration, ease: "easeOut" }
});
const ProductSections = () => {
const shouldReduce = useReducedMotion();
const sections = [
{
id: 1,
title: 'VUCA EVO',
subtitle: 'pinion',
description: 'MAXIMUM PERFORMANCE WITH A MINIMUM OF MAINTENANCE',
buttonText: 'SEE ALL MODELS',
image: 'https://www.bulls-bikes.com/media/image/bulls-vuca-evo-pinion-section.jpg',
textColor: 'text-white',
buttonStyle: 'btn-secondary'
},
{
id: 2,
title: 'NO LIMITS!',
subtitle: '',
description: '',
buttonText: 'CHOOSE YOUR BIKE',
image: 'https://www.bulls-bikes.com/media/image/bulls-no-limits-section.jpg',
textColor: 'text-white',
buttonStyle: 'btn-secondary'
},
{
id: 3,
title: 'MAXIMUM OFFROAD POWER',
subtitle: '',
description: '',
buttonText: 'CHOOSE YOUR E-BIKE',
image: 'https://www.bulls-bikes.com/media/image/bulls-maximum-offroad-power-section.jpg',
textColor: 'text-white',
buttonStyle: 'btn-secondary'
}
];
const SectionCard = ({ section, index }) => {
const content = (
<div
className="relative h-96 md:h-[500px] bg-cover bg-center flex items-center justify-center text-center"
style={{
backgroundImage: `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('${section.image}')`
}}
>
<div className="max-w-2xl px-4">
{section.subtitle && (
<h3 className={`text-2xl md:text-3xl font-light ${section.textColor} mb-2 text-shadow`}>
{section.subtitle}
</h3>
)}
<h2 className={`text-4xl md:text-5xl lg:text-6xl font-bold ${section.textColor} mb-4 text-shadow`}>
{section.title}
</h2>
{section.description && (
<p className={`text-lg md:text-xl ${section.textColor} mb-8 text-shadow`}>
{section.description}
</p>
)}
<button className={`${section.buttonStyle} inline-flex items-center space-x-2`}>
<span>{section.buttonText}</span>
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div>
);
if (shouldReduce) {
return content;
}
return (
<motion.div {...fadeUpPreset(index * 0.2, 1.0)}>
{content}
</motion.div>
);
};
return (
<div className="space-y-0">
{sections.map((section, index) => (
<SectionCard key={section.id} section={section} index={index} />
))}
</div>
);
};
export default ProductSections;

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

@@ -0,0 +1,129 @@
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 bikes = [
{
id: 1,
name: 'COPPERHEAD EVO 1',
price: 'from 3,199.00*',
image: 'https://www.bulls-bikes.com/media/image/bulls-copperhead-evo-1-product.jpg',
rating: 5,
colors: ['#333', '#666', '#999']
},
{
id: 2,
name: 'EVO CX 1',
price: '4,799.00*',
image: 'https://www.bulls-bikes.com/media/image/bulls-evo-cx-1-product.jpg',
rating: 5,
colors: ['#333', '#666']
},
{
id: 3,
name: 'GRINDER 3',
price: '4,499.00*',
image: 'https://www.bulls-bikes.com/media/image/bulls-grinder-3-product.jpg',
rating: 5,
colors: ['#333', '#666', '#999', '#ccc']
}
];
const TopEBikes = () => {
const shouldReduce = useReducedMotion();
const BikeCard = ({ bike, index }) => {
const content = (
<div className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300">
<div className="aspect-w-16 aspect-h-12 bg-gray-100">
<img
src={bike.image}
alt={bike.name}
className="w-full h-64 object-cover"
/>
</div>
<div className="p-6">
<div className="flex items-center mb-2">
{[...Array(bike.rating)].map((_, i) => (
<Star key={i} className="w-4 h-4 fill-yellow-400 text-yellow-400" />
))}
</div>
<h3 className="text-xl font-bold text-bulls-dark mb-2">{bike.name}</h3>
<p className="text-lg font-semibold text-bulls-blue mb-4">{bike.price}</p>
<div className="flex space-x-2 mb-4">
{bike.colors.map((color, i) => (
<div
key={i}
className="w-6 h-6 rounded-full border-2 border-gray-300"
style={{ backgroundColor: color }}
></div>
))}
</div>
</div>
</div>
);
if (shouldReduce) {
return content;
}
return (
<motion.div {...fadeUpPreset(index * 0.1, 0.8)}>
{content}
</motion.div>
);
};
const sectionContent = (
<section className="py-16 bg-white">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-bulls-dark mb-4">TOP E-BIKES</h2>
<div className="flex justify-center space-x-2 mb-8">
<div className="w-3 h-3 bg-bulls-blue rounded-full"></div>
<div className="w-3 h-3 bg-gray-300 rounded-full"></div>
<div className="w-3 h-3 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={bike.id} bike={bike} index={index} />
))}
</div>
<div className="text-center mt-12">
<div className="flex justify-center space-x-1">
{[...Array(8)].map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${
i === 0 ? 'bg-bulls-blue' : 'bg-gray-300'
}`}
></div>
))}
</div>
</div>
</div>
</section>
);
if (shouldReduce) {
return sectionContent;
}
return (
<motion.div {...fadeUpPreset(0.1, 1.0)}>
{sectionContent}
</motion.div>
);
};
export default TopEBikes;

48
src/index.css Normal file
View File

@@ -0,0 +1,48 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Arial', 'Helvetica', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
scroll-behavior: smooth;
}
}
@layer components {
.btn-primary {
@apply bg-bulls-blue text-white px-6 py-3 rounded-md hover:bg-blue-600 transition-colors duration-300 font-medium;
}
.btn-secondary {
@apply bg-white text-bulls-dark px-6 py-3 rounded-md hover:bg-gray-100 transition-colors duration-300 font-medium border border-gray-200;
}
.section-title {
@apply text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4;
}
.section-subtitle {
@apply text-xl md:text-2xl text-white mb-8;
}
}
@layer utilities {
.text-shadow {
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.bg-overlay {
background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4));
}
}

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}",
"./public/index.html"
],
theme: {
extend: {
colors: {
'bulls-gray': '#2d2d2d',
'bulls-light-gray': '#f5f5f5',
'bulls-blue': '#00a8e6',
'bulls-dark': '#1a1a1a',
'bulls-text': '#333333'
},
fontFamily: {
'sans': ['Arial', 'Helvetica', 'sans-serif']
},
backgroundImage: {
'hero-gradient': 'linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4))'
}
},
},
plugins: [],
}

5
vercel.json Normal file
View File

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