Skip to content

Commit

Permalink
Implement favorites functionality (#23)
Browse files Browse the repository at this point in the history
* Update plan.md

* Use enum AppActionTypes

* Remove unused page scan-card.

* Implement mark favorite action.

* Cleanup in reducer.spec.ts

* Implement mark favorite

* Rename filterByQuery.ts to filter.ts

* Add favoriteFilter function

* Implement ToggleShowFavoritesOnlyAction

* Add plan to my cards

* Move my cards page content to my card comp.

* Filter cards by favorite prop.

* Remove unused filterByQuery

* Rename my-cards to my-cards-page.

* Extract cards to CardsList comp.

* Handle no carts states.

* Add a bug

* Adjust names.
  • Loading branch information
lukasbicus authored Nov 13, 2024
1 parent a9501e2 commit 0e1e7d1
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 95 deletions.
8 changes: 6 additions & 2 deletions app/(homescreens)/add-cards/predefined-companies-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { filterByQuery } from '@/app/lib/filterByQuery';
import { getNameFilter } from '@/app/lib/filters';
import { predefinedCompanies } from '@/app/lib/predefined-companies';
import { Routes } from '@/app/lib/shared';
import Image from 'next/image';
Expand All @@ -10,9 +10,13 @@ import { useSearchParams } from 'next/navigation';
export function PredefinedCompaniesList() {
const searchParams = useSearchParams();
const query = searchParams.get('query')?.toString();

const companies = query
? predefinedCompanies.filter(getNameFilter(query))
: predefinedCompanies;
return (
<ul className="menu menu-lg rounded-box text-base-content">
{filterByQuery(predefinedCompanies, query).map(company => (
{companies.map(company => (
<li key={company.name}>
<Link
href={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
'use client';

import useAppState from '@/app/lib/app-state/app-state';
import { filterByQuery } from '@/app/lib/filterByQuery';
import { AppActions, AppActionTypes, Card } from '@/app/lib/app-state/reducer';
import { Routes } from '@/app/lib/shared';
import { IconStar } from '@tabler/icons-react';
import { CompanyIcon } from '@/app/ui/company-icon';
import { IconStar, IconStarFilled } from '@tabler/icons-react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';

export default function MyCards() {
const searchParams = useSearchParams();
const query = searchParams.get('query')?.toString();
const [state] = useAppState();
import { Dispatch } from 'react';

export function CardsList({
cards,
dispatch,
}: {
cards: Card[];
dispatch: Dispatch<AppActions>;
}) {
return (
<ul className="menu menu-sm rounded-box gap-2">
{filterByQuery(state.cards, query).map(card => (
{cards.map(card => (
<li key={card.id}>
<Link
href={{
Expand All @@ -35,8 +36,23 @@ export default function MyCards() {
<CompanyIcon {...card} />
</span>
<span className="text-xl">{card.name}</span>
<button className="btn btn-ghost btn-square btn-primary">
<IconStar className="h-6 w-6" />
<button
className="btn btn-ghost btn-circle btn-primary"
onClick={e => {
e.preventDefault();
dispatch({
type: AppActionTypes.ToggleCardFavorite,
payload: {
id: card.id,
},
});
}}
>
{card.favorite ? (
<IconStarFilled className="h-6 w-6" />
) : (
<IconStar className="h-6 w-6" />
)}
</button>
</Link>
</li>
Expand Down
98 changes: 98 additions & 0 deletions app/(homescreens)/my-cards/my-cards-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import { CardsList } from '@/app/(homescreens)/my-cards/cards-list';
import useAppState from '@/app/lib/app-state/app-state';
import { AppActionTypes, AppState, Card } from '@/app/lib/app-state/reducer';
import { favoriteFilter, getNameFilter } from '@/app/lib/filters';
import { Routes } from '@/app/lib/shared';
import { MainMessage } from '@/app/ui/main-message';
import { PageTemplate } from '@/app/ui/page-template';
import { PrimaryHeader } from '@/app/ui/primary-header';
import { Search } from '@/app/ui/search';
import { IconStarFilled, IconStarHalfFilled } from '@tabler/icons-react';
import { filter, overEvery } from 'lodash';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';

function getVisibleCards(appState: AppState, query: string): Card[] {
const filterFunctions: ((c: Card) => boolean)[] = [];
if (query) {
filterFunctions.push(getNameFilter<Card>(query));
}
if (appState.showFavoritesOnly) {
filterFunctions.push(favoriteFilter<Card>);
}
const combinedFilter: (c: Card) => boolean = overEvery(filterFunctions);

return filter(appState.cards, combinedFilter);
}

export default function MyCardsPage() {
const searchParams = useSearchParams();
const query = searchParams.get('query')?.toString() ?? '';
const [state, dispatch] = useAppState();

// add showFavoritesOnly to app state
// implement toggle on showFavoritesOnly
// if showFavoritesOnly is valid, filter cards by favoriteFilter
// if query, filter cards by queryFilter

const visibleCards = getVisibleCards(state, query);

return (
<PageTemplate
header={
<PrimaryHeader
title="My cards"
actions={
<>
<button
className="btn btn-circle btn-ghost btn-primary "
onClick={() => {
dispatch({
type: AppActionTypes.ToggleShowFavoritesOnly,
});
}}
>
{state.showFavoritesOnly ? (
<IconStarFilled className="w-6 h-6" />
) : (
<IconStarHalfFilled className="w-6 h-6" />
)}
</button>
</>
}
>
{state.cards.length !== 0 ? (
<Search className="flex-1" />
) : (
<div className="h-16" />
)}
</PrimaryHeader>
}
>
{state.cards.length === 0 ? (
<MainMessage
title="Welcome to Tilda"
description="Looks like you haven't added any loyalty cards yet. Get started by adding your first cards! You can manually add card details or import them if you have a digital file."
>
<div className="flex justify-center justify-self-center gap-4 px-4 w-full">
<Link href={Routes.AddCards} className="btn btn-primary w-1/4">
Add cards
</Link>
<Link href={Routes.ImportData} className="btn btn-primary w-1/4">
Import cards
</Link>
</div>
</MainMessage>
) : visibleCards.length === 0 ? (
<MainMessage
title="No cards found"
description="No cards found for given filter. Adjust your filter to see other cards."
/>
) : (
<CardsList cards={visibleCards} dispatch={dispatch} />
)}
</PageTemplate>
);
}
40 changes: 4 additions & 36 deletions app/(homescreens)/my-cards/page.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,11 @@
import MyCards from '@/app/(homescreens)/my-cards/my-cards';
import { Routes } from '@/app/lib/shared';
import MyCardsPage from '@/app/(homescreens)/my-cards/my-cards-page';
import { Loading } from '@/app/ui/loading';
import { PageTemplate } from '@/app/ui/page-template';
import { PrimaryHeader } from '@/app/ui/primary-header';
import { Search } from '@/app/ui/search';
import { IconLayoutGrid, IconStar } from '@tabler/icons-react';
import Link from 'next/link';
import { Suspense } from 'react';

export default function Page() {
return (
<PageTemplate
header={
<PrimaryHeader
title="My cards"
actions={
<>
<Link href={Routes.CreateCard}>
<button className="btn btn-square btn-primary btn-outline">
<IconStar className="w-6 h-6" />
</button>
</Link>
<Link href={Routes.CreateCard}>
<button className="btn btn-square btn-primary btn-outline">
<IconLayoutGrid className="w-6 h-6" />
</button>
</Link>
</>
}
>
<Suspense fallback={<Loading />}>
<Search className="flex-1" />
</Suspense>
</PrimaryHeader>
}
>
<Suspense fallback={<Loading />}>
<MyCards />
</Suspense>
</PageTemplate>
<Suspense fallback={<Loading />}>
<MyCardsPage />
</Suspense>
);
}
3 changes: 2 additions & 1 deletion app/card/card-detail-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { AppActionTypes } from '@/app/lib/app-state/reducer';
import { CodePicture } from '@/app/ui/code-picture';
import { ConfirmDialog } from '@/app/ui/confirm-dialog';
import useAppState from '@/app/lib/app-state/app-state';
Expand Down Expand Up @@ -113,7 +114,7 @@ export function CardDetailPage() {
confirmButtonLabel="Delete"
onConfirmButtonClick={() => {
dispatch({
type: 'DELETE_CARD',
type: AppActionTypes.DeleteCard,
payload: { id: card.id },
});
router.replace(Routes.MyCards);
Expand Down
3 changes: 2 additions & 1 deletion app/create-card/create-card-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@/app/create-card/createCardFormReducer';
import useAppState from '@/app/lib/app-state/app-state';
import { mapHtml5QrcodeFormatToJsbarcodeFormat } from '@/app/lib/app-state/codeFormat';
import { AppActionTypes } from '@/app/lib/app-state/reducer';
import { predefinedCompanies } from '@/app/lib/predefined-companies';
import {
CardIcon,
Expand Down Expand Up @@ -138,7 +139,7 @@ export default function CreateCardForm() {
className="px-4 py-6 w-full h-full"
onSubmit={handleSubmit(data => {
appDispatch({
type: 'ADD_CARD',
type: AppActionTypes.AddCard,
payload: {
name: data[CreateCardFormNames.Name],
code: data[CreateCardFormNames.Code],
Expand Down
3 changes: 2 additions & 1 deletion app/import-data/import-data-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ParseCardsErrors,
} from '@/app/import-data/utils';
import useAppState from '@/app/lib/app-state/app-state';
import { AppActionTypes } from '@/app/lib/app-state/reducer';
import { Routes } from '@/app/lib/shared';
import { ConfirmDialog } from '@/app/ui/confirm-dialog';
import { useErrorDialog } from '@/app/ui/error-dialog-context';
Expand Down Expand Up @@ -53,7 +54,7 @@ export function ImportDataPage() {
});
} else {
dispatch({
type: 'IMPORT_CARDS',
type: AppActionTypes.ImportCards,
payload: uniqParsedCards,
});
// display count of added records in dialog
Expand Down
Loading

0 comments on commit 0e1e7d1

Please sign in to comment.