diff --git a/backend/src/main/java/com/angel/autonow/company/CompanyController.java b/backend/src/main/java/com/angel/autonow/company/CompanyController.java index 5e866de5..f80da9eb 100644 --- a/backend/src/main/java/com/angel/autonow/company/CompanyController.java +++ b/backend/src/main/java/com/angel/autonow/company/CompanyController.java @@ -48,7 +48,7 @@ public CompanyResponseDTO getCompanyById(@PathVariable Long id) { } @GetMapping - @PreAuthorize("hasRole('ADMIN')") + @PreAuthorize("hasAnyRole('ADMIN', 'CUSTOMER', 'DRIVER', 'COMPANY_ADMIN')") public List getAllCompanies() { return companyService.getAllCompanies(); } diff --git a/backend/src/test/java/com/angel/autonow/company/CompanyControllerIT.java b/backend/src/test/java/com/angel/autonow/company/CompanyControllerIT.java index 133cec6e..eee45b2c 100644 --- a/backend/src/test/java/com/angel/autonow/company/CompanyControllerIT.java +++ b/backend/src/test/java/com/angel/autonow/company/CompanyControllerIT.java @@ -171,10 +171,10 @@ void getAllCompanies_noEntries_returnsEmptyList() throws Exception { } @Test - void getAllCompanies_asCustomer_returnsForbidden() throws Exception { + void getAllCompanies_asCustomer_returnsOk() throws Exception { mockMvc.perform(get("/api/companies") .with(TestData.customerJwt())) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } @Test diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6c58e077..1115f14f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,14 @@ "name": "frontend", "version": "1.0.0", "dependencies": { + "@expo/vector-icons": "^15.1.1", "@react-navigation/bottom-tabs": "^7.15.9", "@react-navigation/native": "^7.2.2", "@react-navigation/native-stack": "^7.14.11", "@tanstack/react-query": "^5.99.0", "axios": "^1.15.0", "expo": "~54.0.33", + "expo-blur": "^55.0.14", "expo-constants": "~18.0.13", "expo-localization": "~17.0.8", "expo-secure-store": "~15.0.8", @@ -2247,6 +2249,17 @@ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, + "node_modules/@expo/vector-icons": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", + "license": "MIT", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/ws-tunnel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", @@ -4767,6 +4780,17 @@ } } }, + "node_modules/expo-blur": { + "version": "55.0.14", + "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-55.0.14.tgz", + "integrity": "sha512-NKyCKFWTNpX4CZSsiE1sgkqk/yvR1K0UTukuIbxVKoobB+yALLg1CFav0NqfdQqjhtoj5oEzP0Brlq92Z08Zfg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "18.0.13", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", @@ -4781,6 +4805,21 @@ "react-native": "*" } }, + "node_modules/expo-font": { + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.6.tgz", + "integrity": "sha512-x9czUA3UQWjIwa0ZUEs/eWJNqB4mAue/m4ltESlNPLZhHL0nWWqIfsyHmklTLFH7mVfcHSJvew6k+pR2FE1zVw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-localization": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz", @@ -5083,17 +5122,6 @@ } } }, - "node_modules/expo/node_modules/@expo/vector-icons": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", - "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", - "license": "MIT", - "peerDependencies": { - "expo-font": ">=14.0.4", - "react": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 83a78be5..1fadd043 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,14 @@ "web": "expo start --web" }, "dependencies": { + "@expo/vector-icons": "^15.1.1", "@react-navigation/bottom-tabs": "^7.15.9", "@react-navigation/native": "^7.2.2", "@react-navigation/native-stack": "^7.14.11", "@tanstack/react-query": "^5.99.0", "axios": "^1.15.0", "expo": "~54.0.33", + "expo-blur": "^55.0.14", "expo-constants": "~18.0.13", "expo-localization": "~17.0.8", "expo-secure-store": "~15.0.8", diff --git a/frontend/src/api/companies.ts b/frontend/src/api/companies.ts new file mode 100644 index 00000000..17ae61a7 --- /dev/null +++ b/frontend/src/api/companies.ts @@ -0,0 +1,7 @@ +import client from "./client"; +import type { Company } from "@/types/api"; + +export async function getCompanies(): Promise { + const response = await client.get("/api/companies"); + return response.data; +} diff --git a/frontend/src/components/AccessibilityMenu/AccessibilityMenu.styles.ts b/frontend/src/components/AccessibilityMenu/AccessibilityMenu.styles.ts index b2d09357..404fca2a 100644 --- a/frontend/src/components/AccessibilityMenu/AccessibilityMenu.styles.ts +++ b/frontend/src/components/AccessibilityMenu/AccessibilityMenu.styles.ts @@ -4,18 +4,16 @@ import {borderRadius, fontSize, spacing, ThemeColors} from "@/constants/theme"; export const createStyles = (colors: ThemeColors) => StyleSheet.create({ trigger: { - paddingHorizontal: spacing.sm + 4, + paddingHorizontal: spacing.md, paddingVertical: spacing.sm, - marginRight: spacing.sm, - backgroundColor: colors.surfaceLight, - borderRadius: borderRadius.sm, - borderWidth: 1, - borderColor: colors.border, + marginRight: spacing.md, + borderRadius: 20, + backgroundColor: colors.primary, }, triggerText: { - fontSize: fontSize.md, + fontSize: fontSize.sm, fontWeight: "600", - color: colors.text, + color: colors.white, }, overlay: { flex: 1, diff --git a/frontend/src/components/AccessibilityMenu/AccessibilityMenu.tsx b/frontend/src/components/AccessibilityMenu/AccessibilityMenu.tsx index b059be3d..06d79b1e 100644 --- a/frontend/src/components/AccessibilityMenu/AccessibilityMenu.tsx +++ b/frontend/src/components/AccessibilityMenu/AccessibilityMenu.tsx @@ -22,6 +22,8 @@ export default function AccessibilityMenu() { const triggerRef = useRef(null); const { locale, setLocale } = useLocaleStore(); const { mode, colors, toggleMode } = useThemeStore(); + const showBlobs = useThemeStore((s) => s.showBlobs); + const toggleBlobs = useThemeStore((s) => s.toggleBlobs); const { t } = useTranslation(); const styles = useMemo(() => createStyles(colors), [colors]); @@ -59,8 +61,6 @@ export default function AccessibilityMenu() { setOpen(false)}> - {/* Dark Mode Section */} - {t("common.darkMode")} {t("common.darkMode")} + + {t("common.blobs")} + + + {showBlobs ? t("common.on") : t("common.off")} + + + + - {/* Language Section */} {t("common.language")} {LANGUAGES.map(({ code, labelKey }) => { const active = locale === code; diff --git a/frontend/src/components/BackButton/BackButton.styles.ts b/frontend/src/components/BackButton/BackButton.styles.ts new file mode 100644 index 00000000..89cd5c99 --- /dev/null +++ b/frontend/src/components/BackButton/BackButton.styles.ts @@ -0,0 +1,16 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize, fontWeight} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + button: { + paddingHorizontal: spacing.md, + paddingTop: spacing.md, + alignSelf: "flex-start", + }, + text: { + fontSize: fontSize.lg, + color: colors.primary, + fontWeight: fontWeight.medium, + }, + }); diff --git a/frontend/src/components/BackButton/BackButton.tsx b/frontend/src/components/BackButton/BackButton.tsx new file mode 100644 index 00000000..57ceb9e0 --- /dev/null +++ b/frontend/src/components/BackButton/BackButton.tsx @@ -0,0 +1,20 @@ +import {useMemo} from "react"; +import {TouchableOpacity, Text} from "react-native"; +import {useThemeStore} from "@/stores/themeStore"; +import {createStyles} from "./BackButton.styles"; + +type Props = { + label: string; + onPress: () => void; +}; + +export default function BackButton({label, onPress}: Props) { + const colors = useThemeStore((s) => s.colors); + const styles = useMemo(() => createStyles(colors), [colors]); + + return ( + + {"<"} {label} + + ); +} diff --git a/frontend/src/components/BackgroundShapes/BackgroundShapes.styles.ts b/frontend/src/components/BackgroundShapes/BackgroundShapes.styles.ts new file mode 100644 index 00000000..0d8e5f8d --- /dev/null +++ b/frontend/src/components/BackgroundShapes/BackgroundShapes.styles.ts @@ -0,0 +1,8 @@ +import { StyleSheet } from "react-native"; + +export const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + overflow: "hidden", + }, +}); diff --git a/frontend/src/components/BackgroundShapes/BackgroundShapes.tsx b/frontend/src/components/BackgroundShapes/BackgroundShapes.tsx new file mode 100644 index 00000000..c9e328c0 --- /dev/null +++ b/frontend/src/components/BackgroundShapes/BackgroundShapes.tsx @@ -0,0 +1,60 @@ +import { View, useWindowDimensions } from "react-native"; +import { useMemo } from "react"; +import { useThemeStore } from "@/stores/themeStore"; +import { styles } from "./BackgroundShapes.styles"; + +type Blob = { + top: number; + left: number; + width: number; + height: number; + rotation: string; + color: string; + borderRadii: [number, number, number, number]; + opacity: number; + scale?: [number, number]; +}; + +function createBlobs(W: number, H: number): Blob[] { + return [ + { top: -80, left: -90, width: 220, height: 180, rotation: "-25deg", color: "#00E8C5", borderRadii: [70, 40, 80, 30], opacity: 0.7 }, + { top: -40, left: W - 120, width: 160, height: 200, rotation: "35deg", color: "#FF6B6B", borderRadii: [50, 80, 35, 65], opacity: 0.6 }, + { top: 0.4 * H, left: -70, width: 150, height: 170, rotation: "55deg", color: "#aa9cfe", borderRadii: [65, 45, 75, 35], opacity: 0.5 }, + { top: 0.6 * H, left: W - 80, width: 170, height: 150, rotation: "-40deg", color: "#FFB347", borderRadii: [40, 70, 50, 80], opacity: 0.55 }, + { top: 0.85 * H, left: -60, width: 180, height: 160, rotation: "20deg", color: "#FF8FA3", borderRadii: [75, 35, 60, 50], opacity: 0.5 }, + ]; +} + +export default function BackgroundShapes() { + useThemeStore((s) => s.colors); + const { width: W, height: H } = useWindowDimensions(); + const blobs = useMemo(() => createBlobs(W, H), [W, H]); + + return ( + + {blobs.map((blob, i) => ( + + ))} + + ); +} diff --git a/frontend/src/components/CompanyCard/CompanyCard.styles.ts b/frontend/src/components/CompanyCard/CompanyCard.styles.ts new file mode 100644 index 00000000..d8f3f73c --- /dev/null +++ b/frontend/src/components/CompanyCard/CompanyCard.styles.ts @@ -0,0 +1,56 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize, borderRadius, fontWeight} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + card: { + backgroundColor: colors.glass, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.glassBorder, + overflow: "hidden", + marginTop: spacing.sm, + }, + image: { + width: "100%", + height: 160, + }, + content: { + padding: spacing.md, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: spacing.xs, + }, + name: { + fontSize: fontSize.xl, + fontWeight: fontWeight.semibold, + color: colors.text, + flexShrink: 1, + }, + badge: { + backgroundColor: colors.glassSelected, + borderWidth: 1, + borderColor: colors.glassSelectedBorder, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.sm, + marginLeft: spacing.sm, + }, + badgeText: { + fontSize: fontSize.xs, + fontWeight: fontWeight.medium, + color: colors.primary, + }, + description: { + fontSize: fontSize.md, + color: colors.textSecondary, + marginBottom: spacing.sm, + }, + address: { + fontSize: fontSize.sm, + color: colors.textDisabled, + }, + }); diff --git a/frontend/src/components/CompanyCard/CompanyCard.tsx b/frontend/src/components/CompanyCard/CompanyCard.tsx new file mode 100644 index 00000000..21d8d6ec --- /dev/null +++ b/frontend/src/components/CompanyCard/CompanyCard.tsx @@ -0,0 +1,41 @@ +import {useMemo} from "react"; +import {View, Text, Image} from "react-native"; +import {useThemeStore} from "@/stores/themeStore"; +import {createStyles} from "./CompanyCard.styles"; +import type {Company} from "@/types/api"; + +type Props = { + company: Company; + typeLabel: string; +}; + +export default function CompanyCard({company, typeLabel}: Props) { + const colors = useThemeStore((s) => s.colors); + const styles = useMemo(() => createStyles(colors), [colors]); + + return ( + + {company.logoUrl && ( + + )} + + + + {company.name} + + + {typeLabel} + + + {company.description && ( + + {company.description} + + )} + + {company.address} + + + + ); +} diff --git a/frontend/src/components/GlassCard/GlassCard.styles.ts b/frontend/src/components/GlassCard/GlassCard.styles.ts new file mode 100644 index 00000000..2f8d1fca --- /dev/null +++ b/frontend/src/components/GlassCard/GlassCard.styles.ts @@ -0,0 +1,23 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, borderRadius} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + card: { + backgroundColor: colors.glass, + borderRadius: borderRadius.lg + 4, + borderWidth: 1, + borderColor: colors.glassBorder, + padding: spacing.xl, + shadowColor: colors.black, + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.1, + shadowRadius: 12, + elevation: 4, + }, + cardPressed: { + backgroundColor: colors.glassSelected, + borderColor: colors.glassSelectedBorder, + transform: [{scale: 0.96}], + }, + }); diff --git a/frontend/src/components/GlassCard/GlassCard.tsx b/frontend/src/components/GlassCard/GlassCard.tsx new file mode 100644 index 00000000..7eeb8fd6 --- /dev/null +++ b/frontend/src/components/GlassCard/GlassCard.tsx @@ -0,0 +1,33 @@ +import {useMemo, useState, ReactNode} from "react"; +import {View, TouchableOpacity, StyleProp, ViewStyle} from "react-native"; +import {useThemeStore} from "@/stores/themeStore"; +import {createStyles} from "./GlassCard.styles"; + +type Props = { + children: ReactNode; + onPress?: () => void; + style?: StyleProp; + pressedStyle?: StyleProp; +}; + +export default function GlassCard({children, onPress, style, pressedStyle}: Props) { + const colors = useThemeStore((s) => s.colors); + const styles = useMemo(() => createStyles(colors), [colors]); + const [pressed, setPressed] = useState(false); + + if (onPress) { + return ( + setPressed(true)} + onPressOut={() => setPressed(false)} + activeOpacity={1} + > + {children} + + ); + } + + return {children}; +} diff --git a/frontend/src/components/LogoutButton/LogoutButton.styles.ts b/frontend/src/components/LogoutButton/LogoutButton.styles.ts new file mode 100644 index 00000000..36230bc8 --- /dev/null +++ b/frontend/src/components/LogoutButton/LogoutButton.styles.ts @@ -0,0 +1,19 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + button: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: colors.error, + alignItems: "center", + justifyContent: "center", + marginRight: spacing.md, + }, + icon: { + fontSize: 18, + color: colors.white, + }, + }); diff --git a/frontend/src/components/LogoutButton/LogoutButton.tsx b/frontend/src/components/LogoutButton/LogoutButton.tsx new file mode 100644 index 00000000..07e27b1d --- /dev/null +++ b/frontend/src/components/LogoutButton/LogoutButton.tsx @@ -0,0 +1,18 @@ +import {useMemo} from "react"; +import {TouchableOpacity} from "react-native"; +import {Ionicons} from "@expo/vector-icons"; +import {useThemeStore} from "@/stores/themeStore"; +import {useAuthStore} from "@/stores/authStore"; +import {createStyles} from "./LogoutButton.styles"; + +export default function LogoutButton() { + const colors = useThemeStore((s) => s.colors); + const clearAuth = useAuthStore((s) => s.clearAuth); + const styles = useMemo(() => createStyles(colors), [colors]); + + return ( + + + + ); +} diff --git a/frontend/src/components/ScreenContainer/ScreenContainer.styles.ts b/frontend/src/components/ScreenContainer/ScreenContainer.styles.ts new file mode 100644 index 00000000..3ad26458 --- /dev/null +++ b/frontend/src/components/ScreenContainer/ScreenContainer.styles.ts @@ -0,0 +1,14 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + centered: { + justifyContent: "center", + alignItems: "center", + }, + }); diff --git a/frontend/src/components/ScreenContainer/ScreenContainer.tsx b/frontend/src/components/ScreenContainer/ScreenContainer.tsx new file mode 100644 index 00000000..adaa5fa7 --- /dev/null +++ b/frontend/src/components/ScreenContainer/ScreenContainer.tsx @@ -0,0 +1,23 @@ +import {useMemo, ReactNode} from "react"; +import {View} from "react-native"; +import {useThemeStore} from "@/stores/themeStore"; +import BackgroundShapes from "@/components/BackgroundShapes/BackgroundShapes"; +import {createStyles} from "./ScreenContainer.styles"; + +type Props = { + children: ReactNode; + centered?: boolean; +}; + +export default function ScreenContainer({children, centered}: Props) { + const colors = useThemeStore((s) => s.colors); + const showBlobs = useThemeStore((s) => s.showBlobs); + const styles = useMemo(() => createStyles(colors), [colors]); + + return ( + + {showBlobs && } + {children} + + ); +} diff --git a/frontend/src/components/SearchInput/SearchInput.styles.ts b/frontend/src/components/SearchInput/SearchInput.styles.ts new file mode 100644 index 00000000..f8985e72 --- /dev/null +++ b/frontend/src/components/SearchInput/SearchInput.styles.ts @@ -0,0 +1,21 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize, borderRadius} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + container: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + }, + input: { + borderWidth: 1, + borderColor: colors.inputBorder, + borderRadius: borderRadius.md, + paddingHorizontal: spacing.md, + paddingVertical: 12, + marginTop: spacing.md, + fontSize: fontSize.lg, + backgroundColor: colors.inputBackground, + color: colors.text, + }, + }); diff --git a/frontend/src/components/SearchInput/SearchInput.tsx b/frontend/src/components/SearchInput/SearchInput.tsx new file mode 100644 index 00000000..20dd440a --- /dev/null +++ b/frontend/src/components/SearchInput/SearchInput.tsx @@ -0,0 +1,28 @@ +import {useMemo} from "react"; +import {View, TextInput} from "react-native"; +import {useThemeStore} from "@/stores/themeStore"; +import {createStyles} from "./SearchInput.styles"; + +type Props = { + value: string; + onChangeText: (text: string) => void; + placeholder: string; +}; + +export default function SearchInput({value, onChangeText, placeholder}: Props) { + const colors = useThemeStore((s) => s.colors); + const styles = useMemo(() => createStyles(colors), [colors]); + + return ( + + + + ); +} diff --git a/frontend/src/constants/locales/bg.json b/frontend/src/constants/locales/bg.json index 0398874b..18e1f899 100644 --- a/frontend/src/constants/locales/bg.json +++ b/frontend/src/constants/locales/bg.json @@ -14,9 +14,20 @@ }, "home": { "tab": "Начало", - "title": "Добре дошли в AutoNow", - "placeholder": "Вашите пътувания ще се появят тук", - "logout": "Изход" + "searchPlaceholder": "Търси компании...", + "noServices": "Няма налични компании в момента", + "noResults": "Няма компании, отговарящи на търсенето", + "selectService": "Изберете услуга", + "selectOption": "Как искате да продължите?", + "browseCompanies": "Преглед на компании", + "browseCompaniesDesc": "Изберете от наличните доставчици", + "serviceTypes": { + "TAXI": "Такси", + "LOGISTICS": "Логистика", + "AMBULANCE": "Медицински транспорт", + "MOVING": "Преместване", + "RENTAL": "Рент-а-кар" + } }, "errors": { "emailRequired": "Имейлът е задължителен", @@ -31,6 +42,7 @@ "english": "English", "bulgarian": "Български", "on": "Вкл", - "off": "Изкл" + "off": "Изкл", + "blobs": "Фонови петна" } } diff --git a/frontend/src/constants/locales/en.json b/frontend/src/constants/locales/en.json index e5991e84..91d799d1 100644 --- a/frontend/src/constants/locales/en.json +++ b/frontend/src/constants/locales/en.json @@ -14,9 +14,20 @@ }, "home": { "tab": "Home", - "title": "Welcome to AutoNow", - "placeholder": "Your rides will appear here", - "logout": "Log Out" + "searchPlaceholder": "Search companies...", + "noServices": "No companies available at the moment", + "noResults": "No companies match your search", + "selectService": "Select a service", + "selectOption": "How would you like to proceed?", + "browseCompanies": "Browse Companies", + "browseCompaniesDesc": "Choose from available providers", + "serviceTypes": { + "TAXI": "Taxi", + "LOGISTICS": "Logistics", + "AMBULANCE": "Medical Transport", + "MOVING": "Moving", + "RENTAL": "Car Rental" + } }, "errors": { "emailRequired": "Email is required", @@ -31,6 +42,7 @@ "english": "English", "bulgarian": "Bulgarian", "on": "On", - "off": "Off" + "off": "Off", + "blobs": "Background Blobs" } } diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index 51e99041..4f28edc5 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -31,6 +31,11 @@ export const lightColors = { white: "#FFFFFF", black: "#1A1A1A", transparent: "transparent", + + glass: "rgba(255, 255, 255, 0.6)", + glassBorder: "rgba(255, 255, 255, 0.8)", + glassSelected: "rgba(45, 106, 79, 0.25)", + glassSelectedBorder: "rgba(45, 106, 79, 0.4)", } as const; export const darkColors: ThemeColors = { @@ -64,6 +69,11 @@ export const darkColors: ThemeColors = { white: "#FFFFFF", black: "#121212", transparent: "transparent", + + glass: "rgba(30, 30, 30, 0.6)", + glassBorder: "rgba(255, 255, 255, 0.15)", + glassSelected: "rgba(82, 183, 136, 0.25)", + glassSelectedBorder: "rgba(82, 183, 136, 0.4)", } as const; export const spacing = { diff --git a/frontend/src/hooks/useCompanies.ts b/frontend/src/hooks/useCompanies.ts new file mode 100644 index 00000000..7231b018 --- /dev/null +++ b/frontend/src/hooks/useCompanies.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getCompanies } from "@/api/companies"; + +export function useCompanies() { + return useQuery({ + queryKey: ["companies"], + queryFn: getCompanies, + }); +} diff --git a/frontend/src/navigation/HomeStack.tsx b/frontend/src/navigation/HomeStack.tsx new file mode 100644 index 00000000..ee15ed64 --- /dev/null +++ b/frontend/src/navigation/HomeStack.tsx @@ -0,0 +1,26 @@ +import {useMemo} from "react"; +import {createNativeStackNavigator} from "@react-navigation/native-stack"; +import {useThemeStore} from "@/stores/themeStore"; +import type {HomeStackParamList} from "@/types/navigation"; +import SelectServiceScreen from "@/screens/SelectService/SelectServiceScreen"; +import ServiceOptionsScreen from "@/screens/ServiceOptions/ServiceOptionsScreen"; +import BrowseCompaniesScreen from "@/screens/BrowseCompanies/BrowseCompaniesScreen"; + +const Stack = createNativeStackNavigator(); + +export default function HomeStack() { + const colors = useThemeStore((s) => s.colors); + + const screenOptions = useMemo(() => ({ + headerShown: false, + contentStyle: {backgroundColor: colors.background}, + }), [colors]); + + return ( + + + + + + ); +} diff --git a/frontend/src/navigation/MainTabs.tsx b/frontend/src/navigation/MainTabs.tsx index 956bb58c..e92c249d 100644 --- a/frontend/src/navigation/MainTabs.tsx +++ b/frontend/src/navigation/MainTabs.tsx @@ -1,36 +1,48 @@ -import { useMemo } from "react"; -import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import type { MainTabParamList } from "@/types/navigation"; -import { useThemeStore } from "@/stores/themeStore"; -import { useTranslation } from "@/hooks/useTranslation"; +import {useMemo} from "react"; +import {View} from "react-native"; +import {createBottomTabNavigator} from "@react-navigation/bottom-tabs"; +import type {MainTabParamList} from "@/types/navigation"; +import {useThemeStore} from "@/stores/themeStore"; +import {useTranslation} from "@/hooks/useTranslation"; import AccessibilityMenu from "@/components/AccessibilityMenu/AccessibilityMenu"; -import { createHeaderScreenOptions, createTabBarStyles, createTabBarColors } from "./navigation.styles"; -import HomeScreen from "@/screens/Home/HomeScreen"; +import LogoutButton from "@/components/LogoutButton/LogoutButton"; +import {createHeaderScreenOptions, createHeaderTitleStyle} from "./navigation.styles"; +import HomeStack from "./HomeStack"; const Tab = createBottomTabNavigator(); +function HeaderRight() { + return ( + + + + + ); +} + export default function MainTabs() { - const colors = useThemeStore((s) => s.colors); - const { t } = useTranslation(); - const headerOptions = useMemo(() => createHeaderScreenOptions(colors), [colors]); - const tabBarStyles = useMemo(() => createTabBarStyles(colors), [colors]); - const tabBarColors = useMemo(() => createTabBarColors(colors), [colors]); + const colors = useThemeStore((s) => s.colors); + const {t} = useTranslation(); + const headerOptions = useMemo(() => createHeaderScreenOptions(colors), [colors]); + const headerTitleStyle = useMemo(() => createHeaderTitleStyle(colors), [colors]); - return ( - , - tabBarStyle: tabBarStyles.tabBar, - tabBarActiveTintColor: tabBarColors.activeTintColor, - tabBarInactiveTintColor: tabBarColors.inactiveTintColor, - }} - > - - - ); + return ( + null, + headerRight: () => , + headerRightContainerStyle: {marginRight: 0, paddingRight: 0}, + tabBarStyle: {display: "none"}, + }} + > + + + ); } diff --git a/frontend/src/navigation/navigation.styles.ts b/frontend/src/navigation/navigation.styles.ts index 0f3ab986..20abdf4c 100644 --- a/frontend/src/navigation/navigation.styles.ts +++ b/frontend/src/navigation/navigation.styles.ts @@ -1,13 +1,24 @@ -import { StyleSheet } from "react-native"; +import { StyleSheet, TextStyle } from "react-native"; import { ThemeColors } from "@/constants/theme"; export const createHeaderScreenOptions = (colors: ThemeColors) => ({ headerTitle: "", headerShadowVisible: false, - headerStyle: { backgroundColor: colors.background }, + headerStyle: { + backgroundColor: colors.surface, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.border, + }, }) as const; +export const createHeaderTitleStyle = (colors: ThemeColors): TextStyle => + ({ + color: colors.text, + fontSize: 22, + fontWeight: "700", + }); + export const createTabBarStyles = (colors: ThemeColors) => StyleSheet.create({ tabBar: { diff --git a/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.styles.ts b/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.styles.ts new file mode 100644 index 00000000..d13b5b69 --- /dev/null +++ b/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.styles.ts @@ -0,0 +1,25 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + listContent: { + paddingHorizontal: spacing.md, + paddingBottom: spacing.lg, + }, + centered: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + emptyText: { + fontSize: fontSize.lg, + color: colors.textDisabled, + textAlign: "center", + }, + errorText: { + fontSize: fontSize.lg, + color: colors.error, + textAlign: "center", + }, + }); diff --git a/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.tsx b/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.tsx new file mode 100644 index 00000000..8a1c282a --- /dev/null +++ b/frontend/src/screens/BrowseCompanies/BrowseCompaniesScreen.tsx @@ -0,0 +1,93 @@ +import {useMemo, useState, useCallback} from "react"; +import {Text, FlatList, ActivityIndicator} from "react-native"; +import {useNavigation, useRoute} from "@react-navigation/native"; +import type {NativeStackNavigationProp} from "@react-navigation/native-stack"; +import type {RouteProp} from "@react-navigation/native"; +import {useThemeStore} from "@/stores/themeStore"; +import {useTranslation} from "@/hooks/useTranslation"; +import {useCompanies} from "@/hooks/useCompanies"; +import ScreenContainer from "@/components/ScreenContainer/ScreenContainer"; +import SearchInput from "@/components/SearchInput/SearchInput"; +import CompanyCard from "@/components/CompanyCard/CompanyCard"; +import BackButton from "@/components/BackButton/BackButton"; +import {createStyles} from "./BrowseCompaniesScreen.styles"; +import type {HomeStackParamList} from "@/types/navigation"; +import type {Company} from "@/types/api"; + +type NavigationProp = NativeStackNavigationProp; +type ScreenRouteProp = RouteProp; + +export default function BrowseCompaniesScreen() { + const colors = useThemeStore((s) => s.colors); + const {t} = useTranslation(); + const styles = useMemo(() => createStyles(colors), [colors]); + const navigation = useNavigation(); + const route = useRoute(); + const {serviceType} = route.params; + + const {data: companies, isLoading, error} = useCompanies(); + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + if (!companies) return []; + const byType = companies.filter((c) => c.companyType === serviceType); + if (!search.trim()) return byType; + const query = search.trim().toLowerCase(); + return byType.filter((c) => c.name.toLowerCase().includes(query)); + }, [companies, serviceType, search]); + + const renderCompany = useCallback( + ({item}: {item: Company}) => ( + + ), + [t], + ); + + const keyExtractor = useCallback((item: Company) => String(item.id), []); + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + {error.message} + + ); + } + + const emptyMessage = search.trim() ? t("home.noResults") : t("home.noServices"); + + return ( + + navigation.goBack()} + /> + + {emptyMessage}} + /> + + ); +} diff --git a/frontend/src/screens/Home/HomeScreen.styles.ts b/frontend/src/screens/Home/HomeScreen.styles.ts deleted file mode 100644 index 6ac6db35..00000000 --- a/frontend/src/screens/Home/HomeScreen.styles.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { StyleSheet } from "react-native"; -import { ThemeColors, spacing, fontSize, borderRadius } from "@/constants/theme"; - -export const createStyles = (colors: ThemeColors) => - StyleSheet.create({ - container: { - flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: colors.background, - paddingHorizontal: spacing.lg, - }, - title: { - fontSize: fontSize.xxl, - fontWeight: "700", - color: colors.text, - marginBottom: spacing.sm, - }, - email: { - fontSize: fontSize.lg, - color: colors.primary, - marginBottom: spacing.md, - }, - placeholder: { - fontSize: fontSize.md, - color: colors.textDisabled, - marginBottom: spacing.xxl, - }, - logoutButton: { - paddingHorizontal: spacing.xl, - paddingVertical: 14, - borderRadius: borderRadius.md, - borderWidth: 1, - borderColor: colors.error, - }, - logoutText: { - color: colors.error, - fontSize: fontSize.lg, - fontWeight: "600", - }, - }); diff --git a/frontend/src/screens/Home/HomeScreen.tsx b/frontend/src/screens/Home/HomeScreen.tsx deleted file mode 100644 index 256480c0..00000000 --- a/frontend/src/screens/Home/HomeScreen.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useMemo } from "react"; -import { View, Text, TouchableOpacity } from "react-native"; -import { useAuthStore } from "@/stores/authStore"; -import { useThemeStore } from "@/stores/themeStore"; -import { useTranslation } from "@/hooks/useTranslation"; -import { createStyles } from "./HomeScreen.styles"; - -export default function HomeScreen() { - const userEmail = useAuthStore((s) => s.userEmail); - const clearAuth = useAuthStore((s) => s.clearAuth); - const colors = useThemeStore((s) => s.colors); - const { t } = useTranslation(); - const styles = useMemo(() => createStyles(colors), [colors]); - - return ( - - {t("home.title")} - {userEmail && {userEmail}} - {t("home.placeholder")} - - - {t("home.logout")} - - - ); -} diff --git a/frontend/src/screens/SelectService/SelectServiceScreen.styles.ts b/frontend/src/screens/SelectService/SelectServiceScreen.styles.ts new file mode 100644 index 00000000..d2438ee6 --- /dev/null +++ b/frontend/src/screens/SelectService/SelectServiceScreen.styles.ts @@ -0,0 +1,37 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize, fontWeight} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + wrapper: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + title: { + fontSize: fontSize.xxl, + fontWeight: fontWeight.bold, + color: colors.text, + paddingBottom: spacing.lg, + textAlign: "center", + }, + grid: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + gap: spacing.md, + maxWidth: 340, + }, + card: { + width: 150, + minHeight: 120, + alignItems: "center", + justifyContent: "center", + }, + cardText: { + fontSize: fontSize.xl, + fontWeight: fontWeight.semibold, + color: colors.text, + textAlign: "center", + }, + }); diff --git a/frontend/src/screens/SelectService/SelectServiceScreen.tsx b/frontend/src/screens/SelectService/SelectServiceScreen.tsx new file mode 100644 index 00000000..3c1a46cf --- /dev/null +++ b/frontend/src/screens/SelectService/SelectServiceScreen.tsx @@ -0,0 +1,47 @@ +import {useMemo} from "react"; +import {View, Text} from "react-native"; +import {useNavigation} from "@react-navigation/native"; +import type {NativeStackNavigationProp} from "@react-navigation/native-stack"; +import {useThemeStore} from "@/stores/themeStore"; +import {useTranslation} from "@/hooks/useTranslation"; +import ScreenContainer from "@/components/ScreenContainer/ScreenContainer"; +import GlassCard from "@/components/GlassCard/GlassCard"; +import {createStyles} from "./SelectServiceScreen.styles"; +import type {HomeStackParamList} from "@/types/navigation"; +import type {CompanyType} from "@/types/api"; + +const SERVICE_TYPES: CompanyType[] = ["TAXI", "LOGISTICS", "AMBULANCE", "MOVING", "RENTAL"]; + +type NavigationProp = NativeStackNavigationProp; + +export default function SelectServiceScreen() { + const colors = useThemeStore((s) => s.colors); + const {t} = useTranslation(); + const styles = useMemo(() => createStyles(colors), [colors]); + const navigation = useNavigation(); + + const handleSelect = (type: CompanyType) => { + navigation.navigate("ServiceOptions", {serviceType: type}); + }; + + return ( + + + {t("home.selectService")} + + {SERVICE_TYPES.map((type) => ( + handleSelect(type)} + style={styles.card} + > + + {t(`home.serviceTypes.${type}`)} + + + ))} + + + + ); +} diff --git a/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.styles.ts b/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.styles.ts new file mode 100644 index 00000000..5781a835 --- /dev/null +++ b/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.styles.ts @@ -0,0 +1,39 @@ +import {StyleSheet} from "react-native"; +import {ThemeColors, spacing, fontSize, fontWeight} from "@/constants/theme"; + +export const createStyles = (colors: ThemeColors) => + StyleSheet.create({ + wrapper: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + title: { + fontSize: fontSize.xxl, + fontWeight: fontWeight.bold, + color: colors.text, + paddingBottom: spacing.lg, + textAlign: "center", + }, + container: { + paddingHorizontal: spacing.lg, + alignItems: "center", + width: "100%", + }, + card: { + width: "100%", + maxWidth: 320, + }, + cardTitle: { + fontSize: fontSize.xl, + fontWeight: fontWeight.semibold, + color: colors.text, + marginBottom: spacing.xs, + textAlign: "center", + }, + cardDescription: { + fontSize: fontSize.md, + color: colors.textSecondary, + textAlign: "center", + }, + }); diff --git a/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.tsx b/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.tsx new file mode 100644 index 00000000..ad5ce645 --- /dev/null +++ b/frontend/src/screens/ServiceOptions/ServiceOptionsScreen.tsx @@ -0,0 +1,46 @@ +import {useMemo} from "react"; +import {View, Text} from "react-native"; +import {useNavigation, useRoute} from "@react-navigation/native"; +import type {NativeStackNavigationProp} from "@react-navigation/native-stack"; +import type {RouteProp} from "@react-navigation/native"; +import {useThemeStore} from "@/stores/themeStore"; +import {useTranslation} from "@/hooks/useTranslation"; +import ScreenContainer from "@/components/ScreenContainer/ScreenContainer"; +import GlassCard from "@/components/GlassCard/GlassCard"; +import BackButton from "@/components/BackButton/BackButton"; +import {createStyles} from "./ServiceOptionsScreen.styles"; +import type {HomeStackParamList} from "@/types/navigation"; + +type NavigationProp = NativeStackNavigationProp; +type ScreenRouteProp = RouteProp; + +export default function ServiceOptionsScreen() { + const colors = useThemeStore((s) => s.colors); + const {t} = useTranslation(); + const styles = useMemo(() => createStyles(colors), [colors]); + const navigation = useNavigation(); + const route = useRoute(); + const {serviceType} = route.params; + + const handleBrowseCompanies = () => { + navigation.navigate("BrowseCompanies", {serviceType}); + }; + + return ( + + navigation.goBack()} + /> + + {t("home.selectOption")} + + + {t("home.browseCompanies")} + {t("home.browseCompaniesDesc")} + + + + + ); +} diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index c303cf41..a68806bb 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -5,17 +5,21 @@ import { lightColors, darkColors, ThemeColors } from "@/constants/theme"; export type ThemeMode = "light" | "dark"; const THEME_KEY = "app_theme"; +const BLOBS_KEY = "app_show_blobs"; interface ThemeState { mode: ThemeMode; colors: ThemeColors; + showBlobs: boolean; setMode: (mode: ThemeMode) => void; toggleMode: () => void; + toggleBlobs: () => void; } export const useThemeStore = create((set, get) => ({ mode: "light", colors: lightColors, + showBlobs: true, setMode: (mode) => { set({ mode, colors: mode === "dark" ? darkColors : lightColors }); @@ -26,6 +30,12 @@ export const useThemeStore = create((set, get) => ({ const next = get().mode === "light" ? "dark" : "light"; get().setMode(next); }, + + toggleBlobs: () => { + const next = !get().showBlobs; + set({ showBlobs: next }); + SecureStore.setItemAsync(BLOBS_KEY, String(next)); + }, })); export async function hydrateTheme(): Promise { @@ -33,4 +43,8 @@ export async function hydrateTheme(): Promise { if (stored === "light" || stored === "dark") { useThemeStore.getState().setMode(stored); } + const blobs = await SecureStore.getItemAsync(BLOBS_KEY); + if (blobs === "false") { + useThemeStore.setState({ showBlobs: false }); + } } diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index bc56dc25..7054656c 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -18,3 +18,18 @@ export interface ProblemDetail { [key: string]: unknown; }; } + +export type CompanyType = "TAXI" | "LOGISTICS" | "AMBULANCE" | "MOVING" | "RENTAL"; + +export interface Company { + id: number; + name: string; + address: string; + phone: string; + email: string; + logoUrl: string | null; + description: string | null; + companyType: CompanyType; + createdAt: string; + updatedAt: string; +} diff --git a/frontend/src/types/navigation.ts b/frontend/src/types/navigation.ts index 888a3808..326ee906 100644 --- a/frontend/src/types/navigation.ts +++ b/frontend/src/types/navigation.ts @@ -1,8 +1,16 @@ +import type {CompanyType} from "@/types/api"; + export type AuthStackParamList = { - Login: undefined; - Register: undefined; + Login: undefined; + Register: undefined; +}; + +export type HomeStackParamList = { + SelectService: undefined; + ServiceOptions: {serviceType: CompanyType}; + BrowseCompanies: {serviceType: CompanyType}; }; export type MainTabParamList = { - Home: undefined; + HomeStack: undefined; };