Compare commits
4 Commits
dc26a8c467
...
47ac6f4c74
| Author | SHA1 | Date | |
|---|---|---|---|
| 47ac6f4c74 | |||
| 928f65f9c5 | |||
| 19cba1606b | |||
| 9a11b4ea7e |
@@ -17,10 +17,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import './Footer.css';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useLanguage } from '../hooks/useLanguage';
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useLanguage } from '../hooks/useLanguage';
|
||||
import LanguageSwitcher from './LanguageSwitcher';
|
||||
import './Header.css';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useLanguage } from '../hooks/useLanguage';
|
||||
import './LanguageSwitcher.css';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
|
||||
@@ -1,86 +1,5 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
const LanguageContext = createContext();
|
||||
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
// Header
|
||||
home: 'Home',
|
||||
about: 'About',
|
||||
contact: 'Contact',
|
||||
|
||||
// CTA Section
|
||||
readyToStart: 'Ready to get started?',
|
||||
ctaSubtitle: 'Join thousands of developers building amazing applications with our modern stack.',
|
||||
startBuilding: 'Start Building',
|
||||
|
||||
// Language Switcher
|
||||
language: 'Language',
|
||||
english: 'English',
|
||||
finnish: 'Finnish',
|
||||
|
||||
// Search Page
|
||||
searchPlaceholder: 'Enter your search query...',
|
||||
searchButton: 'Search',
|
||||
searchResults: 'Search Results',
|
||||
searchResultsSubtitle: 'Here are the results for your search query.',
|
||||
searchResultsPlaceholder: 'Search results will appear here. Try entering a search term above to get started.',
|
||||
|
||||
// Search Filters
|
||||
dogFriendly: 'Dog Friendly',
|
||||
accessible: 'Accessible',
|
||||
familyFriendly: 'Family Friendly',
|
||||
peopleCount: 'People',
|
||||
priceRange: 'Price',
|
||||
any: 'Any',
|
||||
|
||||
// Footer
|
||||
footerDescription: 'Find fun things to do!',
|
||||
},
|
||||
fi: {
|
||||
// Header
|
||||
home: 'Koti',
|
||||
about: 'Tietoja',
|
||||
contact: 'Yhteystiedot',
|
||||
|
||||
|
||||
// CTA Section
|
||||
readyToStart: 'Valmiina aloittamaan?',
|
||||
ctaSubtitle: 'Liity tuhansien kehittäjien joukkoon, jotka rakentavat upeita sovelluksia modernilla teknologiapinoamme.',
|
||||
startBuilding: 'Aloita rakentaminen',
|
||||
|
||||
// Language Switcher
|
||||
language: 'Kieli',
|
||||
english: 'Englanti',
|
||||
finnish: 'Suomi',
|
||||
|
||||
// Search Page
|
||||
searchPlaceholder: 'Syötä hakukyselysi...',
|
||||
searchButton: 'Hae',
|
||||
searchResults: 'Hakutulokset',
|
||||
searchResultsSubtitle: 'Tässä ovat hakutulokset kyselysi perusteella.',
|
||||
searchResultsPlaceholder: 'Hakutulokset näkyvät täällä. Kokeile syöttää hakusana yllä aloittaaksesi.',
|
||||
|
||||
// Search Filters
|
||||
dogFriendly: 'Koiraystävällinen',
|
||||
accessible: 'Esteetön',
|
||||
familyFriendly: 'Lapsiystävällinen',
|
||||
peopleCount: 'Henkilöt',
|
||||
priceRange: 'Hinta',
|
||||
any: 'Mikä tahansa',
|
||||
|
||||
// Footer
|
||||
footerDescription: 'Löydä hauskaa tekemistä!',
|
||||
}
|
||||
};
|
||||
import React, { useState } from 'react';
|
||||
import { LanguageContext, translations } from './languageContextData';
|
||||
|
||||
export const LanguageProvider = ({ children }) => {
|
||||
const [language, setLanguage] = useState('en');
|
||||
|
||||
75
frontend/src/contexts/languageContextData.js
Normal file
75
frontend/src/contexts/languageContextData.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const LanguageContext = createContext();
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
// Header
|
||||
home: 'Home',
|
||||
about: 'About',
|
||||
contact: 'Contact',
|
||||
|
||||
// CTA Section
|
||||
readyToStart: 'Ready to get started?',
|
||||
ctaSubtitle: 'Join thousands of developers building amazing applications with our modern stack.',
|
||||
startBuilding: 'Start Building',
|
||||
|
||||
// Language Switcher
|
||||
language: 'Language',
|
||||
english: 'English',
|
||||
finnish: 'Finnish',
|
||||
|
||||
// Search Page
|
||||
searchPlaceholder: 'Enter your search query...',
|
||||
searchButton: 'Search',
|
||||
searchResults: 'Search Results',
|
||||
searchResultsSubtitle: 'Here are the results for your search query.',
|
||||
searchResultsPlaceholder: 'Search results will appear here. Try entering a search term above to get started.',
|
||||
|
||||
// Search Filters
|
||||
dogFriendly: 'Dog Friendly',
|
||||
accessible: 'Accessible',
|
||||
familyFriendly: 'Family Friendly',
|
||||
peopleCount: 'People',
|
||||
priceRange: 'Price',
|
||||
any: 'Any',
|
||||
|
||||
// Footer
|
||||
footerDescription: 'Find fun things to do!',
|
||||
},
|
||||
fi: {
|
||||
// Header
|
||||
home: 'Koti',
|
||||
about: 'Tietoja',
|
||||
contact: 'Yhteystiedot',
|
||||
|
||||
|
||||
// CTA Section
|
||||
readyToStart: 'Valmiina aloittamaan?',
|
||||
ctaSubtitle: 'Liity tuhansien kehittäjien joukkoon, jotka rakentavat upeita sovelluksia modernilla teknologiapinoamme.',
|
||||
startBuilding: 'Aloita rakentaminen',
|
||||
|
||||
// Language Switcher
|
||||
language: 'Kieli',
|
||||
english: 'Englanti',
|
||||
finnish: 'Suomi',
|
||||
|
||||
// Search Page
|
||||
searchPlaceholder: 'Syötä hakukyselysi...',
|
||||
searchButton: 'Hae',
|
||||
searchResults: 'Hakutulokset',
|
||||
searchResultsSubtitle: 'Tässä ovat hakutulokset kyselysi perusteella.',
|
||||
searchResultsPlaceholder: 'Hakutulokset näkyvät täällä. Kokeile syöttää hakusana yllä aloittaaksesi.',
|
||||
|
||||
// Search Filters
|
||||
dogFriendly: 'Koiraystävällinen',
|
||||
accessible: 'Esteetön',
|
||||
familyFriendly: 'Lapsiystävällinen',
|
||||
peopleCount: 'Henkilöt',
|
||||
priceRange: 'Hinta',
|
||||
any: 'Mikä tahansa',
|
||||
|
||||
// Footer
|
||||
footerDescription: 'Löydä hauskaa tekemistä!',
|
||||
}
|
||||
};
|
||||
10
frontend/src/hooks/useLanguage.js
Normal file
10
frontend/src/hooks/useLanguage.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { LanguageContext } from '../contexts/languageContextData';
|
||||
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -205,6 +205,107 @@
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Search Results Section */
|
||||
.search-results {
|
||||
padding: 4rem 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 3rem 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-card .card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-card .card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.result-card .card-body p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.amenities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.amenity-tag {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||
color: var(--color-dark);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px var(--color-shadow-primary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.amenity-tag:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--color-shadow-primary);
|
||||
}
|
||||
|
||||
/* No Results Section */
|
||||
.no-results {
|
||||
padding: 4rem 0;
|
||||
background: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-results-content h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.no-results-content p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.search-button.searching {
|
||||
background: linear-gradient(135deg, var(--color-secondary-dark) 0%, var(--color-dark) 100%);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.search-button.searching:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 4px 12px var(--color-shadow-secondary);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
@@ -261,6 +362,27 @@
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.no-results-content h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -284,6 +406,14 @@
|
||||
min-width: 180px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.no-results-content h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@@ -334,4 +464,24 @@
|
||||
.features {
|
||||
background: var(--color-bg-dark);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
background: var(--color-bg-dark);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
background: var(--color-bg-dark);
|
||||
}
|
||||
|
||||
.results-title {
|
||||
color: var(--color-text-primary-light);
|
||||
}
|
||||
|
||||
.no-results-content h2 {
|
||||
color: var(--color-text-primary-light);
|
||||
}
|
||||
|
||||
.no-results-content p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Header, Footer, Button, Card } from '../components';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useLanguage } from '../hooks/useLanguage';
|
||||
import './Home.css';
|
||||
|
||||
const Home = () => {
|
||||
@@ -13,14 +13,62 @@ const Home = () => {
|
||||
peopleCount: '',
|
||||
priceRange: ''
|
||||
});
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// Mockup data for search results
|
||||
const mockResults = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Cozy Mountain Cabin",
|
||||
subtitle: "€€ • 4 guests • Dog friendly",
|
||||
image: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Beautiful mountain cabin with stunning views, perfect for a peaceful getaway. Features a hot tub and fireplace.",
|
||||
amenities: ["Dog friendly", "Fireplace", "Hot tub", "Mountain view"]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Modern City Apartment",
|
||||
subtitle: "€€€ • 2 guests • Accessible",
|
||||
image: "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Stylish apartment in the heart of the city with modern amenities and easy access to public transportation.",
|
||||
amenities: ["Accessible", "City view", "Gym access", "Parking"]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Family Beach House",
|
||||
subtitle: "€€€€ • 6 guests • Family friendly",
|
||||
image: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Spacious beachfront property perfect for family vacations. Includes a pool, playground, and direct beach access.",
|
||||
amenities: ["Family friendly", "Beach access", "Pool", "Playground"]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Rustic Farmhouse",
|
||||
subtitle: "€€ • 8 guests • Dog friendly",
|
||||
image: "https://images.unsplash.com/photo-1571896349842-33c89424de2d?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Charming farmhouse surrounded by nature. Perfect for large groups looking for a rural escape.",
|
||||
amenities: ["Dog friendly", "Large groups", "Nature", "Fireplace"]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Luxury Villa",
|
||||
subtitle: "€€€€ • 4 guests • Accessible",
|
||||
image: "https://images.unsplash.com/photo-1613490493576-7fde63acd811?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Exclusive villa with premium amenities including a private pool, chef's kitchen, and stunning ocean views.",
|
||||
amenities: ["Accessible", "Private pool", "Ocean view", "Chef's kitchen"]
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Cozy Studio",
|
||||
subtitle: "€ • 2 guests • Family friendly",
|
||||
image: "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
description: "Affordable studio apartment perfect for couples or small families. Located in a quiet neighborhood.",
|
||||
amenities: ["Family friendly", "Affordable", "Quiet", "Central location"]
|
||||
}
|
||||
];
|
||||
|
||||
const handleGetStarted = () => {
|
||||
alert('Get Started button clicked!');
|
||||
};
|
||||
|
||||
const handleLearnMore = () => {
|
||||
alert('Learn More button clicked!');
|
||||
};
|
||||
|
||||
const handleFilterChange = (filterName, value) => {
|
||||
setFilters(prev => ({
|
||||
@@ -30,9 +78,30 @@ const Home = () => {
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// Handle search logic here
|
||||
console.log('Search query:', searchQuery);
|
||||
console.log('Filters:', filters);
|
||||
if (!searchQuery.trim()) {
|
||||
alert('Please enter a search query');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
// Filter results based on search query and filters
|
||||
let filteredResults = mockResults.filter(result => {
|
||||
const matchesQuery = result.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
result.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
const matchesDogFriendly = !filters.dogFriendly || result.amenities.includes('Dog friendly');
|
||||
const matchesAccessible = !filters.accessible || result.amenities.includes('Accessible');
|
||||
const matchesFamilyFriendly = !filters.familyFriendly || result.amenities.includes('Family friendly');
|
||||
|
||||
return matchesQuery && matchesDogFriendly && matchesAccessible && matchesFamilyFriendly;
|
||||
});
|
||||
|
||||
setSearchResults(filteredResults);
|
||||
setIsSearching(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,8 +126,12 @@ const Home = () => {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<button className="search-button" onClick={handleSearch}>
|
||||
{t('searchButton')}
|
||||
<button
|
||||
className={`search-button ${isSearching ? 'searching' : ''}`}
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching}
|
||||
>
|
||||
{isSearching ? 'Searching...' : t('searchButton')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -140,6 +213,50 @@ const Home = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Search Results Section */}
|
||||
{searchResults.length > 0 && (
|
||||
<section className="search-results">
|
||||
<div className="container">
|
||||
<h2 className="results-title">
|
||||
Found {searchResults.length} result{searchResults.length !== 1 ? 's' : ''} for "{searchQuery}"
|
||||
</h2>
|
||||
<div className="results-grid">
|
||||
{searchResults.map(result => (
|
||||
<Card
|
||||
key={result.id}
|
||||
title={result.title}
|
||||
subtitle={result.subtitle}
|
||||
image={result.image}
|
||||
imageAlt={result.title}
|
||||
className="result-card"
|
||||
>
|
||||
<p>{result.description}</p>
|
||||
<div className="amenities">
|
||||
{result.amenities.map((amenity, index) => (
|
||||
<span key={index} className="amenity-tag">
|
||||
{amenity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* No Results Message */}
|
||||
{searchResults.length === 0 && !isSearching && searchQuery && (
|
||||
<section className="no-results">
|
||||
<div className="container">
|
||||
<div className="no-results-content">
|
||||
<h2>No results found for "{searchQuery}"</h2>
|
||||
<p>Try adjusting your search terms or filters to find more options.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user