Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: offline mode #27

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 71 additions & 37 deletions webapp/src/components/cards/OfferCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,96 @@ import { OfferKindBadge } from "../OfferKindBadge";
type OfferCardProps = {
offer: OfferIncluded;
displayExpiryDate?: boolean;
userOffline?: boolean;
};

const OfferCard = ({ offer, displayExpiryDate = false }: OfferCardProps) => {
const OfferCard = ({
offer,
displayExpiryDate = false,
userOffline = false,
}: OfferCardProps) => {
return (
<Link
href={`/dashboard/offer/${
offer.kind === "code" ? "online" : "in-store"
}/${offer.id}`}
href={
userOffline
? {}
: `/dashboard/offer/${
offer.kind === "code" ? "online" : "in-store"
}/${offer.id}`
}
>
<Flex flexDir="column">
<Flex
bgColor={offer.partner.color}
py={5}
borderTopRadius={12}
position="relative"
justifyContent="center"
alignItems="center"
sx={{ ...dottedPattern("#ffffff") }}
>
<Flex alignItems="center" borderRadius="full" p={1} bgColor="white">
<Image
src={offer.partner.icon.url ?? ""}
alt={offer.partner.icon.alt ?? ""}
width={32}
height={32}
/>
{!userOffline && (
<Flex
bgColor={offer.partner.color}
py={5}
borderTopRadius={12}
position="relative"
justifyContent="center"
alignItems="center"
sx={{ ...dottedPattern("#ffffff") }}
>
<Flex alignItems="center" borderRadius="full" p={1} bgColor="white">
<Image
src={offer.partner.icon.url ?? ""}
alt={offer.partner.icon.alt ?? ""}
width={32}
height={32}
/>
</Flex>
</Flex>
</Flex>
)}
<Flex
flexDir="column"
p={3}
bgColor="white"
borderBottomRadius={8}
borderRadius={8}
borderTopRadius={userOffline ? 8 : 0}
gap={2}
boxShadow="md"
>
<Text fontSize="sm" fontWeight="medium">
<Text
fontSize="sm"
fontWeight="medium"
textDecor={userOffline ? "underline" : "none"}
textDecorationColor={offer.partner.color}
textDecorationThickness={"2px"}
>
{offer.partner.name}
</Text>
<Text fontWeight="bold" fontSize="sm" noOfLines={2} h="42px">
<Text
fontWeight={userOffline ? "normal" : "bold"}
fontSize="sm"
noOfLines={2}
h="42px"
>
{offer.title}
</Text>
<OfferKindBadge kind={offer.kind} variant="light" />
{displayExpiryDate && (
<Flex
alignSelf="start"
borderRadius="2xl"
bgColor="bgWhite"
py={2}
px={3}
>
<Text fontSize="xs" fontWeight="medium">
Expire le : {new Date(offer.validityTo).toLocaleDateString()}
</Text>
</Flex>
{userOffline && offer.coupons && offer.coupons[0] && (
<Text my={4} fontWeight={"bold"} fontSize="xl" textAlign={"center"}>
{offer.coupons[0].code}
</Text>
)}
<Flex
flexDirection={userOffline ? "row" : "column"}
gap={2}
justifyContent={"space-between"}
>
<OfferKindBadge kind={offer.kind} variant="light" />
{displayExpiryDate && (
<Flex
alignSelf="start"
borderRadius="2xl"
bgColor="bgWhite"
py={2}
px={3}
>
<Text fontSize="xs" fontWeight="medium">
Expire le : {new Date(offer.validityTo).toLocaleDateString()}
</Text>
</Flex>
)}
</Flex>
</Flex>
</Flex>
</Link>
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { NextRequest } from "next/server";

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/_offline") return NextResponse.next();

if (
!request.cookies.get(process.env.NEXT_PUBLIC_JWT_NAME ?? "cje-jwt") &&
request.nextUrl.pathname.startsWith("/dashboard")
Expand Down
37 changes: 37 additions & 0 deletions webapp/src/pages/_offline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Flex, Heading, Text } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import OfferCard from "~/components/cards/OfferCard";
import { OfferIncluded } from "~/server/api/routers/offer";

const OfflinePage = () => {
const [userOffers, setUserOffers] = useState<OfferIncluded[]>([]);

useEffect(() => {
const storedOffers = localStorage.getItem("cje-user-offers");
if (storedOffers) {
setUserOffers(JSON.parse(storedOffers));
}
}, []);

return (
<Flex flexDir="column" pt={12} px={8} h="full">
<Heading textAlign={"center"} mb={6}>
Pas de réseau...
</Heading>
<Flex flexDir="column" gap={6}>
{userOffers.map((userOffer) => {
return (
<OfferCard
key={userOffer.id}
offer={userOffer}
displayExpiryDate
userOffline
/>
);
})}
</Flex>
</Flex>
);
};

export default OfflinePage;
19 changes: 18 additions & 1 deletion webapp/src/pages/dashboard/offer/online/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { FiBook, FiCopy, FiLink } from "react-icons/fi";
import { IoCloseCircleOutline } from "react-icons/io5";
import { useLocalStorage } from "usehooks-ts";
import LoadingLoader from "~/components/LoadingLoader";
import ToastComponent from "~/components/ToastComponent";
import { CouponIcon } from "~/components/icons/coupon";
import OfferActivationModal from "~/components/modals/OfferActivationModal";
import CouponWrapper from "~/components/wrappers/CouponWrapper";
import OfferWrapper from "~/components/wrappers/OfferWrapper";
import { OfferIncluded } from "~/server/api/routers/offer";
import { couponAnimation } from "~/utils/animations";
import { api } from "~/utils/api";

export default function Dashboard() {
const router = useRouter();
const { id } = router.query;

const [userOffers, setUserOffers] = useLocalStorage<OfferIncluded[]>(
"cje-user-offers",
[]
);

const { data: resultOffer, isLoading: isLoadingOffer } =
api.offer.getById.useQuery(
{
Expand Down Expand Up @@ -58,7 +65,17 @@ export default function Dashboard() {
isLoading,
isSuccess,
} = api.coupon.assignToUser.useMutation({
onSuccess: () => refetchCoupon(),
onSuccess: (response) => {
if (offer)
setUserOffers([
...userOffers,
{
...offer,
coupons: [response.data],
},
]);
refetchCoupon();
},
});

const toast = useToast();
Expand Down
18 changes: 14 additions & 4 deletions webapp/src/pages/dashboard/wallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import {
TabPanel,
Text,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { PiSmileySadFill } from "react-icons/pi";
import { useLocalStorage } from "usehooks-ts";
import LoadingLoader from "~/components/LoadingLoader";
import OfferCard from "~/components/cards/OfferCard";
import WalletWrapper from "~/components/wrappers/WalletWrapper";
import { api } from "~/utils/api";
import { PiSmileySadFill } from "react-icons/pi";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Offer } from "~/payload/payload-types";
import { OfferIncluded } from "~/server/api/routers/offer";
import { api } from "~/utils/api";

const WalletNoData = ({ kind }: { kind: Offer["kind"] }) => {
return (
Expand All @@ -38,6 +40,10 @@ export default function Wallet() {
offerKind?: Offer["kind"];
};

const [userOffers, setUserOffers] = useLocalStorage<OfferIncluded[]>(
"cje-user-offers",
[]
);
const [tabIndex, setTabIndex] = useState(2);

useEffect(() => {
Expand Down Expand Up @@ -81,6 +87,10 @@ export default function Wallet() {
(offer) => offer.kind === "code"
);

useEffect(() => {
if (currentUserOffers) setUserOffers(currentUserOffers);
}, [currentUserOffers]);

if (isLoadingUserOffers) {
return (
<WalletWrapper tabIndex={tabIndex} handleTabsChange={handleTabsChange}>
Expand Down
61 changes: 36 additions & 25 deletions webapp/src/server/api/routers/offer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { PaginatedDocs } from "payload/database";
import { Where, WhereField } from "payload/types";
import { z } from "zod";
import { Category, Offer, Media, Partner } from "~/payload/payload-types";
import {
Category,
Offer,
Media,
Partner,
Coupon,
} from "~/payload/payload-types";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { ZGetListParams } from "~/server/types";

export interface OfferIncluded extends Offer {
partner: Partner & { icon: Media };
category: Category & { icon: Media };
coupons?: Coupon[];
}

export const offerRouter = createTRPCRouter({
Expand Down Expand Up @@ -34,50 +42,53 @@ export const offerRouter = createTRPCRouter({
};
}

const offers = await ctx.payload.find({
const offers = (await ctx.payload.find({
collection: "offers",
limit: perPage,
page: page,
where: where as Where,
sort,
});
})) as PaginatedDocs<OfferIncluded>;

const couponCountOfOffers = await ctx.payload.find({
collection: "coupons",
depth: 0,
limit: 10000,
page: 1,
where: {
offer: {
in: offers.docs.map((offer) => offer.id),
},
},
});

const offersFiltered = offers.docs.filter((offer) => {
const couponFiltered = couponCountOfOffers.docs.filter(
(coupon) => coupon.offer === offer.id
);

let couponCount = 0;
const offersFiltered = offers.docs
.map((offer) => {
const couponFiltered = couponCountOfOffers.docs.filter(
(coupon) => coupon.offer === offer.id
);

if (isCurrentUser) {
couponCount = couponFiltered.filter(
(coupon) => coupon.user === ctx.session.id && coupon.used === false
).length;
} else {
couponCount = couponFiltered.filter(
(coupon) =>
(coupon.user === undefined ||
coupon.user === null ||
coupon.user === ctx.session.id) &&
coupon.used === false
).length;
}
if (isCurrentUser) {
offer.coupons = couponFiltered.filter(
(coupon) =>
coupon.user === ctx.session.id && coupon.used === false
);
} else {
offer.coupons = couponFiltered.filter(
(coupon) =>
(coupon.user === undefined ||
coupon.user === null ||
coupon.user === ctx.session.id) &&
coupon.used === false
);
}

if (couponCount > 0) return offer;
});
return offer;
})
.filter((offer) => !offer.coupons || offer.coupons.length > 0);

return {
data: offersFiltered as OfferIncluded[],
data: offersFiltered,
metadata: { page, count: offers.docs.length },
};
}),
Expand Down
Loading