Skip to content

Commit

Permalink
Merge pull request #8 from fabriciopgl/feature/1
Browse files Browse the repository at this point in the history
✨ Adiciona lógica de favoritos do usuário
  • Loading branch information
fabriciopgl authored Nov 26, 2024
2 parents 91fbd70 + 109bdb8 commit 762fb19
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 215 deletions.
4 changes: 3 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
NEXT_PUBLIC_GOOGLE_CLIENT_ID=xxxx
NEXT_PUBLIC_GOOGLE_USER_SCRIPT_URL=xxx
NEXT_PUBLIC_GOOGLE_USER_SCRIPT_URL=xxxx
NEXT_PUBLIC_GOOGLE_PLACES_SCRIPT_URL=xxxx
NEXT_PUBLIC_GOOGLE_FAVORITE_PLACES_SCRIPT_URL=xxxx
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
env:
NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
NEXT_PUBLIC_GOOGLE_USER_SCRIPT_URL: ${{ secrets.GOOGLE_USER_SCRIPT_URL }}
NEXT_PUBLIC_GOOGLE_PLACES_SCRIPT_URL: ${{ secrets.GOOGLE_PLACES_SCRIPT_URL }}
NEXT_PUBLIC_GOOGLE_FAVORITE_PLACES_SCRIPT_URL: ${{ secrets.GOOGLE_FAVORITE_PLACES_SCRIPT_URL }}
run: yarn build

- name: Deploy to GitHub Pages
Expand Down
1 change: 0 additions & 1 deletion app/favoritos/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// app/routes/about/page.tsx
export default function FavoritesPage() {
return (
<div className="container mx-auto p-4">
Expand Down
57 changes: 43 additions & 14 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use client";
import PlaceCard from "@/components/PlaceCard";
import Filter from "@/components/Filters";
import NavbarSearch from "@/components/NavbarSearch";

import usePlaces, { Place } from "@/hooks/usePlaces";

import PlaceCard from "@/components/PlaceCard";
import { useAuth } from "@/contexts/AuthContext";
import { useFavorites } from "@/contexts/FavoritePlacesContext";
import { usePlacesContext } from "@/contexts/PlacesContext";
import { Place } from "@/domains/Places/types";
import { Button, Card, CardBody } from "@nextui-org/react";
import { useEffect, useState } from "react";
import { FaArrowLeft, FaArrowRight, FaSearch } from "react-icons/fa";

export default function HomePage() {
const { places } = usePlaces();
const { user, isAnonymous } = useAuth();
const { favorites, fetchFavorites, isLoading } = useFavorites();
const { places, loading } = usePlacesContext();
const [currentPage, setCurrentPage] = useState(1);
const [filteredPlaces, setFilteredPlaces] = useState<Place[]>(places);
const [searchValue, setSearchValue] = useState("");
Expand Down Expand Up @@ -45,6 +48,12 @@ export default function HomePage() {
setCurrentPage(1);
}, [searchValue, places]);

useEffect(() => {
if (user && !isAnonymous && favorites.length === 0)
fetchFavorites(user.internalId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, isAnonymous]);

const handleSearch = (value: string) => {
setSearchValue(value);
};
Expand Down Expand Up @@ -74,17 +83,37 @@ export default function HomePage() {
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 pt-8">
{currentPlaces.map((place) => (
<PlaceCard
key={place.id}
name={place.name}
imageUrl={place.image}
description={place.description}
/>
))}
{loading || isLoading
? Array.from({ length: 9 }).map((_, index) => (
<Card
key={index}
className="w-80 h-90 mx-auto shadow-lg animate-pulse"
>
<div className="bg-gray-300 h-[250px] w-full object-cover rounded-t-lg"></div>
<CardBody className="flex flex-col h-full p-4">
<div className="bg-gray-300 h-6 w-3/4 mb-4 rounded"></div>
<div className="bg-gray-300 h-4 w-full mb-4 rounded"></div>
<div className="bg-gray-300 h-4 w-5/6 rounded mb-4 flex-1"></div>
<div className="flex flex-row justify-between items-center gap-2">
<div className="bg-gray-300 h-10 w-full rounded-lg"></div>
<div className="bg-gray-300 h-10 w-10 rounded-full"></div>
</div>
</CardBody>
</Card>
))
: currentPlaces.map((place) => (
<PlaceCard
key={place.id}
id={place.id}
name={place.name}
imageUrl={place.image}
description={place.description}
favorite={favorites?.some((f) => f === place.id)}
/>
))}
</div>

{currentPlaces.length > 0 ? (
{!loading && currentPlaces.length > 0 ? (
<div className="flex justify-center items-center mt-8">
<Button
startContent={<FaArrowLeft />}
Expand Down
9 changes: 7 additions & 2 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { AuthProvider } from "@/contexts/AuthContext";
import { FavoriteProvider } from "@/contexts/FavoritePlacesContext";
import { PlacesProvider } from "@/contexts/PlacesContext";
import { NextUIProvider } from "@nextui-org/system";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
Expand Down Expand Up @@ -28,7 +29,11 @@ const Providers: React.FC<ProvidersProps> = ({ children, themeProps }) => {
return (
<NextUIProvider navigate={router.push}>
<NextThemesProvider disableTransitionOnChange {...themeProps}>
<AuthProvider>{children}</AuthProvider>
<AuthProvider>
<FavoriteProvider>
<PlacesProvider>{children}</PlacesProvider>
</FavoriteProvider>
</AuthProvider>
</NextThemesProvider>
</NextUIProvider>
);
Expand Down
1 change: 0 additions & 1 deletion components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// app/components/Footer.tsx
export default function Footer() {
return (
<footer className="text-center py-4 border-t">
Expand Down
2 changes: 1 addition & 1 deletion components/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ExtendedJwtPayload extends JwtPayload {
}

export default function LoginComponent({ onSuccess }: LoginComponentProps) {
//This variables was previous set in environment variables on Netlify (only for production)
//This variables was previous set in environment variables on GitHub (only for production)
// For development, you need to specify the value in .env.development
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
const scriptUrl = process.env.NEXT_PUBLIC_GOOGLE_USER_SCRIPT_URL!;
Expand Down
2 changes: 0 additions & 2 deletions components/NavbarSearch/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import usePlaces from "@/hooks/usePlaces";
import { Input } from "@nextui-org/input";
import { Kbd } from "@nextui-org/kbd";
import { useEffect, useState } from "react";
import { FaSearch } from "react-icons/fa";

interface NavbarSearchProps {
Expand Down
27 changes: 22 additions & 5 deletions components/PlaceCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
import { Button, Card, CardBody, Image } from "@nextui-org/react";
import { HeartIcon } from "./HeartIcon";
import { useState } from "react";
import { useFavorites } from "@/contexts/FavoritePlacesContext";

type Props = {
id: number;
name: string;
description: string;
imageUrl: string;
favorite: boolean;
};

export default function PlaceCard({ name, description, imageUrl }: Props) {
const [liked, setLiked] = useState(false);
export default function PlaceCard({
id,
name,
description,
imageUrl,
favorite,
}: Props) {
const { addFavorite, removeFavorite, favorites } = useFavorites();
return (
<Card className="w-80 h-90 mx-auto shadow-lg transition-transform transform-gpu hover:scale-105 active:scale-100 cursor-pointer">
<Image
Expand All @@ -34,13 +43,21 @@ export default function PlaceCard({ name, description, imageUrl }: Props) {
className="text-default-900/60 data-[hover]:bg-foreground/10"
radius="full"
variant="light"
onPress={() => setLiked((v) => !v)}
onPress={() => {
const isFavorite = favorites?.includes(id);

if (isFavorite) {
removeFavorite(id);
} else {
addFavorite(id);
}
}}
>
<HeartIcon
className={
liked ? "[&>path]:stroke-transparent fill-red-500" : ""
favorite ? "[&>path]:stroke-transparent fill-red-500" : ""
}
fill={liked ? "currentColor" : "none"}
fill={favorite ? "currentColor" : "none"}
width={undefined}
height={undefined}
/>
Expand Down
101 changes: 101 additions & 0 deletions contexts/FavoritePlacesContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import axios from "axios";
import { useAuth } from "./AuthContext";

type FavoriteContextType = {
favorites: number[];
addFavorite: (placeId: number) => void;
removeFavorite: (placeId: number) => void;
fetchFavorites: (userId: number) => void;
isLoading: boolean;
};

const FavoriteContext = createContext<FavoriteContextType | undefined>(
undefined
);

const SCRIPT_URL = process.env.NEXT_PUBLIC_GOOGLE_FAVORITE_PLACES_SCRIPT_URL!;

export const FavoriteProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [favorites, setFavorites] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);

const { user, isAnonymous } = useAuth();

const fetchFavorites = async (userId: number) => {
try {
setIsLoading(true);
const response = await axios.get(`${SCRIPT_URL}?userId=${userId}`);
setFavorites(response.data);
} catch (error) {
console.error("Error fetching favorites:", error);
} finally {
setIsLoading(false);
}
};

const addFavorite = async (placeId: number) => {
setFavorites((prevFavorites) => {
const updatedFavorites = [...prevFavorites, placeId];
if (user && !isAnonymous) updateFavoriteInSheet(placeId, true);
return updatedFavorites;
});
};

const removeFavorite = async (placeId: number) => {
setFavorites((prevFavorites) => {
const updatedFavorites = prevFavorites.filter((id) => id !== placeId);
if (user && !isAnonymous) updateFavoriteInSheet(placeId, false);
return updatedFavorites;
});
};

const updateFavoriteInSheet = async (
placeId: number,
isFavorite: boolean
) => {
try {
const userId = user?.internalId;
await axios.post(
SCRIPT_URL,
{
userId,
placeId,
isFavorite,
},
{
headers: {
"Content-Type": "text/plain;charset=utf-8",
},
}
);
} catch (error) {
console.error("Error updating favorite in sheet:", error);
}
};

return (
<FavoriteContext.Provider
value={{
favorites,
addFavorite,
removeFavorite,
fetchFavorites,
isLoading,
}}
>
{children}
</FavoriteContext.Provider>
);
};

// Hook para acessar o contexto
export const useFavorites = () => {
const context = useContext(FavoriteContext);
if (!context) {
throw new Error("useFavorites must be used within a FavoriteProvider");
}
return context;
};
56 changes: 56 additions & 0 deletions contexts/PlacesContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";
import { Place } from "@/domains/Places/types";
import React, { createContext, useContext, useEffect, useState } from "react";

interface PlacesContextType {
places: Place[];
loading: boolean;
error: string | null;
}

const PlacesContext = createContext<PlacesContextType | undefined>(undefined);

const SCRIPT_URL = process.env.NEXT_PUBLIC_GOOGLE_PLACES_SCRIPT_URL!;

export const PlacesProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [places, setPlaces] = useState<Place[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchPlaces = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(SCRIPT_URL);
if (!response.ok) {
throw new Error(`Error fetching data: ${response.status}`);
}
const places = await response.json();
setPlaces(places.data || []);
} catch (err: unknown) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};

fetchPlaces();
}, []);

return (
<PlacesContext.Provider value={{ places, loading, error }}>
{children}
</PlacesContext.Provider>
);
};

export const usePlacesContext = () => {
const context = useContext(PlacesContext);
if (!context) {
throw new Error("usePlacesContext deve ser usado dentro de PlacesProvider");
}
return context;
};
9 changes: 9 additions & 0 deletions domains/Places/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type PlaceType = "restaurant" | "atraction" | "route";

export type Place = {
id: number;
name: string;
description: string;
image: string;
type: PlaceType[];
};
Loading

0 comments on commit 762fb19

Please sign in to comment.