Skip to content

Commit 35aa987

Browse files
authored
Merge pull request #34 from fraidakis/fraidakis
Improve "Maintainability Index" metric
2 parents 5d1eff7 + 5a09cc2 commit 35aa987

25 files changed

Lines changed: 1657 additions & 864 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Page State Components
3+
*
4+
* Reusable loading and error state components for consistent UX.
5+
*
6+
* @module components/common/PageStates
7+
*/
8+
9+
import React from 'react';
10+
import { Spinner, Icon, Button } from '../ui';
11+
12+
/**
13+
* Loading state component
14+
* Displays a centered loading spinner with message
15+
*
16+
* @param {Object} props - Component props
17+
* @param {Function} props.t - Translation function
18+
* @returns {React.ReactElement} Loading state UI
19+
*/
20+
export const LoadingState = ({ t }) => (
21+
<div className="place-details-page">
22+
<div className="container">
23+
<div className="loading-state">
24+
<Spinner size="lg" />
25+
<p>{t('loadingDetails')}</p>
26+
</div>
27+
</div>
28+
</div>
29+
);
30+
31+
/**
32+
* Error state component
33+
* Displays an error message with a back button
34+
*
35+
* @param {Object} props - Component props
36+
* @param {Function} props.t - Translation function
37+
* @param {string} props.error - Error message to display
38+
* @param {Function} props.onBack - Back button click handler
39+
* @returns {React.ReactElement} Error state UI
40+
*/
41+
export const ErrorState = ({ t, error, onBack }) => (
42+
<div className="place-details-page">
43+
<div className="container">
44+
<div className="error-state">
45+
<Icon name="alert" size="2xl" />
46+
<h3>{t('placeNotFound')}</h3>
47+
<p>{error}</p>
48+
<Button variant="primary" onClick={onBack}>
49+
{t('back')}
50+
</Button>
51+
</div>
52+
</div>
53+
</div>
54+
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Location Field Component
3+
*
4+
* Reusable location input field with autocomplete functionality.
5+
*
6+
* @module components/navigation/LocationField
7+
*/
8+
9+
import React from 'react';
10+
import { AutocompleteInput } from '../ui';
11+
import { searchLocations } from '../../utils/geocoding';
12+
13+
/**
14+
* LocationField Component
15+
* Generic location input field with section header and autocomplete
16+
*
17+
* @param {Object} props - Component props
18+
* @param {string} props.label - Section header label
19+
* @param {string} props.icon - Icon emoji for section header
20+
* @param {string} props.value - Current input value
21+
* @param {Function} props.onChange - Input change handler
22+
* @param {Function} props.onSelect - Autocomplete selection handler
23+
* @param {boolean} props.loading - Loading state
24+
* @param {string} props.error - Error message
25+
* @param {string} props.placeholder - Input placeholder text
26+
* @param {Function} props.t - Translation function
27+
* @returns {React.ReactElement} Location input field
28+
*/
29+
const LocationField = ({
30+
label,
31+
icon,
32+
value,
33+
onChange,
34+
onSelect,
35+
loading: _ = false,
36+
error = null,
37+
placeholder,
38+
name,
39+
t
40+
}) => {
41+
return (
42+
<div className="location-section">
43+
<div className="section-header">
44+
<span className="section-icon">{icon}</span>
45+
<h4>{label}</h4>
46+
</div>
47+
<div className="form-group">
48+
<label className="form-label">{t('enterLocation')}</label>
49+
<AutocompleteInput
50+
name={name}
51+
value={value}
52+
onChange={onChange}
53+
onSelect={onSelect}
54+
searchFn={searchLocations}
55+
placeholder={placeholder || t('searchLocationPlaceholder') || 'e.g., Athens, Greece'}
56+
icon={null}
57+
required
58+
/>
59+
{error && (
60+
<div className="field-error">
61+
{error}
62+
</div>
63+
)}
64+
</div>
65+
</div>
66+
);
67+
};
68+
69+
export default LocationField;

src/components/navigation/RouteForm.jsx

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66

77
import React from 'react';
8-
import { Button, Icon, Spinner, AutocompleteInput } from '../ui';
9-
import { searchLocations } from '../../utils/geocoding';
8+
import { Button, Icon, Spinner } from '../ui';
9+
import LocationField from './LocationField';
1010

1111
/**
1212
* Route form component for NavigationPage
@@ -43,27 +43,18 @@ const RouteForm = ({
4343
{/* Route form with autocomplete inputs */}
4444
<form onSubmit={onSubmit}>
4545
{/* Start location section */}
46-
<div className="location-section">
47-
<div className="section-header">
48-
<span className="section-icon">📍</span>
49-
<h4>{t('startPoint')}</h4>
50-
</div>
51-
<div className="form-group">
52-
<label className="form-label">{t('enterLocation')}</label>
53-
<AutocompleteInput
54-
name="fromLocation"
55-
value={formData.fromLocation}
56-
onChange={handleInputChange}
57-
onSelect={(suggestion) => {
58-
setFormData(prev => ({ ...prev, fromLocation: suggestion.displayName }));
59-
}}
60-
searchFn={searchLocations}
61-
placeholder={t('searchLocationPlaceholder') || 'e.g., Athens, Greece'}
62-
icon={null}
63-
required
64-
/>
65-
</div>
66-
</div>
46+
<LocationField
47+
name="fromLocation"
48+
label={t('startPoint')}
49+
icon="📍"
50+
value={formData.fromLocation}
51+
onChange={handleInputChange}
52+
onSelect={(suggestion) => {
53+
setFormData(prev => ({ ...prev, fromLocation: suggestion.displayName }));
54+
}}
55+
placeholder={t('searchLocationPlaceholder') || 'e.g., Athens, Greece'}
56+
t={t}
57+
/>
6758

6859
<div className="location-connector">
6960
<div className="connector-line"></div>
@@ -72,27 +63,18 @@ const RouteForm = ({
7263
</div>
7364

7465
{/* Destination location section */}
75-
<div className="location-section">
76-
<div className="section-header">
77-
<span className="section-icon">🎯</span>
78-
<h4>{t('destination')}</h4>
79-
</div>
80-
<div className="form-group">
81-
<label className="form-label">{t('enterLocation')}</label>
82-
<AutocompleteInput
83-
name="toLocation"
84-
value={formData.toLocation}
85-
onChange={handleInputChange}
86-
onSelect={(suggestion) => {
87-
setFormData(prev => ({ ...prev, toLocation: suggestion.displayName }));
88-
}}
89-
searchFn={searchLocations}
90-
placeholder={t('searchLocationPlaceholder') || 'e.g., Thessaloniki, Greece'}
91-
icon={null}
92-
required
93-
/>
94-
</div>
95-
</div>
66+
<LocationField
67+
name="toLocation"
68+
label={t('destination')}
69+
icon="🎯"
70+
value={formData.toLocation}
71+
onChange={handleInputChange}
72+
onSelect={(suggestion) => {
73+
setFormData(prev => ({ ...prev, toLocation: suggestion.displayName }));
74+
}}
75+
placeholder={t('searchLocationPlaceholder') || 'e.g., Thessaloniki, Greece'}
76+
t={t}
77+
/>
9678

9779
{/* Transport mode selection */}
9880
<div className="transport-section">

src/components/place/PlaceHero.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Place Hero Component
3+
*
4+
* Hero section for place details page displaying place name, category, location, and rating.
5+
*
6+
* @module components/place/PlaceHero
7+
*/
8+
9+
import React from 'react';
10+
import { Icon, Badge } from '../ui';
11+
12+
/**
13+
* PlaceHero Component
14+
* Displays the hero section of a place details page
15+
*
16+
* @param {Object} props - Component props
17+
* @param {Object} props.place - Place object
18+
* @param {number|null} props.averageRating - Average rating value
19+
* @param {number} props.reviewsCount - Number of reviews
20+
* @param {Function} props.onBack - Back button handler
21+
* @param {Function} props.t - Translation function
22+
* @returns {React.ReactElement} Place hero section
23+
*/
24+
const PlaceHero = ({ place, averageRating, reviewsCount, onBack, t }) => {
25+
return (
26+
<div className="place-hero">
27+
<div className="place-hero-overlay"></div>
28+
<div className="container">
29+
<div className="place-hero-content animate-fadeInUp">
30+
{/* Back navigation button */}
31+
<button type="button" className="back-link" onClick={onBack}>
32+
<Icon name="arrow" size="md" style={{ transform: 'rotate(180deg)' }} />
33+
{t('back')}
34+
</button>
35+
36+
{/* Category badge display */}
37+
<div className="place-category-badge">
38+
<Badge variant="accent" size="lg">
39+
{t(place.category) || place.category}
40+
</Badge>
41+
</div>
42+
43+
{/* Place title */}
44+
<h1 className="place-title" data-cy="place-title">
45+
{place.name}
46+
</h1>
47+
48+
{/* Meta information: location and rating */}
49+
<div className="place-meta">
50+
<span className="meta-item">
51+
<span className="meta-icon">📍</span>
52+
{place.city}, {place.country}
53+
</span>
54+
<span className="meta-item meta-item--rating">
55+
<span className="meta-icon"></span>
56+
<span className="rating-value">
57+
{averageRating ? averageRating.toFixed(1) : 'N/A'}
58+
</span>
59+
<span className="rating-count">
60+
({reviewsCount} {t('reviews')})
61+
</span>
62+
</span>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
);
68+
};
69+
70+
export default PlaceHero;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Place Info Card Component
3+
*
4+
* Information card displaying place address and description.
5+
*
6+
* @module components/place/PlaceInfoCard
7+
*/
8+
9+
import React from 'react';
10+
import { Icon } from '../ui';
11+
12+
/**
13+
* PlaceInfoCard Component
14+
* Displays place information including address, description, and report option
15+
*
16+
* @param {Object} props - Component props
17+
* @param {Object} props.place - Place object with address and description
18+
* @param {Function} props.onReport - Report button click handler
19+
* @param {Function} props.t - Translation function
20+
* @returns {React.ReactElement} Place info card
21+
*/
22+
const PlaceInfoCard = ({ place, onReport, t }) => {
23+
return (
24+
<div className="place-info-card" data-cy="place-description-card">
25+
<h2>
26+
<Icon name="info" size="sm" />
27+
{t('information')}
28+
</h2>
29+
30+
<div className="place-address-row">
31+
<span className="info-icon">📍</span>
32+
<span className="info-value">{place.address}</span>
33+
</div>
34+
35+
<div className="place-description-text">
36+
<p>{place.description}</p>
37+
</div>
38+
39+
{/* Report button for flagging inappropriate content */}
40+
<button onClick={onReport} className="report-link">
41+
<Icon name="alert" size="xs" />
42+
{t('report')}
43+
</button>
44+
</div>
45+
);
46+
};
47+
48+
export default PlaceInfoCard;

src/components/preferences/PreferenceForm.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const PreferenceForm = ({ t, formData, setFormData, editingProfile, onSubmit, on
5151
className="form-input"
5252
placeholder="e.g., Weekend Getaway"
5353
required
54+
pattern=".*\S+.*"
55+
title="Profile name cannot be empty or contain only whitespace"
5456
/>
5557
</div>
5658

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Profiles Grid Component
3+
*
4+
* Displays grid of preference profiles or empty state.
5+
* @module components/preferences/ProfilesGrid
6+
*/
7+
8+
import React from 'react';
9+
import { Button } from '../ui';
10+
import ProfileCard from './ProfileCard';
11+
12+
/**
13+
* Renders grid of profiles or empty state
14+
*/
15+
const ProfilesGrid = ({ profiles, activeProfileId, t, onActivate, onEdit, onDelete, onCreateClick }) => {
16+
if (profiles.length === 0) {
17+
return (
18+
<div className="empty-state">
19+
<div className="empty-icon">⚙️</div>
20+
<h3>{t('noPreferenceProfiles')}</h3>
21+
<p>{t('createFirstProfile')}</p>
22+
<Button onClick={onCreateClick} variant="primary" size="lg">
23+
{t('createProfile')}
24+
</Button>
25+
</div>
26+
);
27+
}
28+
29+
return (
30+
<div className="profiles-grid">
31+
{profiles.map((profile, index) => {
32+
const isActive = profile.profileId === activeProfileId || profile.id === activeProfileId;
33+
return (
34+
<ProfileCard
35+
key={profile.id || profile.profileId}
36+
t={t}
37+
profile={profile}
38+
isActive={isActive}
39+
index={index}
40+
onActivate={() => onActivate(profile.profileId || profile.id)}
41+
onEdit={() => onEdit(profile)}
42+
onDelete={() => onDelete(profile.profileId || profile.id)}
43+
/>
44+
);
45+
})}
46+
</div>
47+
);
48+
};
49+
50+
export default ProfilesGrid;

0 commit comments

Comments
 (0)