From faaffdbb176358638e2ea5dfe2e421d622980c7d Mon Sep 17 00:00:00 2001 From: Oliver Levay Date: Tue, 22 Aug 2023 18:08:20 +0200 Subject: [PATCH 1/2] :bug: Fix bug where quantity does not include chest items --- backend/services/core/graphql.schema.json | 12 ++++++++++ ...655_product_id_from_user_inventory_item.ts | 13 ++++++++++ .../core/src/datasources/WebshopAPI.ts | 1 + .../services/core/src/schemas/webshop.graphql | 1 + .../shared/converters/webshopConverters.ts | 1 + backend/services/core/src/types/graphql.ts | 2 ++ backend/services/core/src/types/webshop.ts | 1 + frontend/api/chest.graphql | 1 + frontend/components/Webshop/Product.tsx | 24 +++++++++++++------ frontend/generated/graphql.tsx | 4 +++- 10 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 backend/services/core/migrations/20230822152655_product_id_from_user_inventory_item.ts diff --git a/backend/services/core/graphql.schema.json b/backend/services/core/graphql.schema.json index 52f1c0d8a..3d9b17ed2 100644 --- a/backend/services/core/graphql.schema.json +++ b/backend/services/core/graphql.schema.json @@ -13670,6 +13670,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "productId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "variant", "description": null, diff --git a/backend/services/core/migrations/20230822152655_product_id_from_user_inventory_item.ts b/backend/services/core/migrations/20230822152655_product_id_from_user_inventory_item.ts new file mode 100644 index 000000000..ca4d88978 --- /dev/null +++ b/backend/services/core/migrations/20230822152655_product_id_from_user_inventory_item.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('user_inventory_item', (table) => { + table.uuid('product_id').nullable().references('product.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('user_inventory_item', (table) => { + table.dropColumn('product_id'); + }); +} diff --git a/backend/services/core/src/datasources/WebshopAPI.ts b/backend/services/core/src/datasources/WebshopAPI.ts index cdea8f6bf..f2212952c 100644 --- a/backend/services/core/src/datasources/WebshopAPI.ts +++ b/backend/services/core/src/datasources/WebshopAPI.ts @@ -665,6 +665,7 @@ export default class WebshopAPI extends dbUtils.KnexDataSource { variant: productInventory.variant, user_inventory_id: userInventory.id, product_inventory_id: orderItem.product_inventory_id, + product_id: product.id, category_id: product.category_id, })); } diff --git a/backend/services/core/src/schemas/webshop.graphql b/backend/services/core/src/schemas/webshop.graphql index 05d5a8b68..46ad32ff4 100644 --- a/backend/services/core/src/schemas/webshop.graphql +++ b/backend/services/core/src/schemas/webshop.graphql @@ -48,6 +48,7 @@ type UserInventory { type UserInventoryItem { id: UUID! + productId: UUID name: String! description: String! paidPrice: Float! diff --git a/backend/services/core/src/shared/converters/webshopConverters.ts b/backend/services/core/src/shared/converters/webshopConverters.ts index 36bcf1301..12016e766 100644 --- a/backend/services/core/src/shared/converters/webshopConverters.ts +++ b/backend/services/core/src/shared/converters/webshopConverters.ts @@ -96,4 +96,5 @@ export const convertUserInventoryItem = ( paidAt: item.paid_at, paidPrice: item.paid_price, consumedAt: item.consumed_at, + productId: item.product_id, }); diff --git a/backend/services/core/src/types/graphql.ts b/backend/services/core/src/types/graphql.ts index 4a55ff6b3..5d2ae049a 100644 --- a/backend/services/core/src/types/graphql.ts +++ b/backend/services/core/src/types/graphql.ts @@ -1770,6 +1770,7 @@ export type UserInventoryItem = { name: Scalars['String']['output']; paidAt: Scalars['Date']['output']; paidPrice: Scalars['Float']['output']; + productId?: Maybe; variant?: Maybe; }; @@ -3057,6 +3058,7 @@ export type UserInventoryItemResolvers; paidAt?: Resolver; paidPrice?: Resolver; + productId?: Resolver, ParentType, ContextType>; variant?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }>; diff --git a/backend/services/core/src/types/webshop.ts b/backend/services/core/src/types/webshop.ts index ea2eb090d..62cb7afaf 100644 --- a/backend/services/core/src/types/webshop.ts +++ b/backend/services/core/src/types/webshop.ts @@ -108,6 +108,7 @@ export interface UserInventoryItem { id: UUID, user_inventory_id: UUID, product_inventory_id: UUID, + product_id: UUID, category_id: UUID, student_id: UUID, name: string, diff --git a/frontend/api/chest.graphql b/frontend/api/chest.graphql index e41b733e6..5762b6946 100644 --- a/frontend/api/chest.graphql +++ b/frontend/api/chest.graphql @@ -15,6 +15,7 @@ query MyChest($studentId: String!) { } paidAt consumedAt + productId } } } diff --git a/frontend/components/Webshop/Product.tsx b/frontend/components/Webshop/Product.tsx index 902e7cfb8..e717aaa1b 100644 --- a/frontend/components/Webshop/Product.tsx +++ b/frontend/components/Webshop/Product.tsx @@ -18,18 +18,24 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { MyCartQuery, - ProductsQuery, useAddToMyCartMutation, useMyCartQuery, useProductsQuery, + MyChestQuery, + ProductsQuery, useAddToMyCartMutation, useMyCartQuery, useMyChestQuery, useProductsQuery, } from '~/generated/graphql'; import handleApolloError from '~/functions/handleApolloError'; import { useSnackbar } from '~/providers/SnackbarProvider'; import { useApiAccess } from '~/providers/ApiAccessProvider'; +import { useUser } from '~/providers/UserProvider'; -function getQuantityInMyCart(productId: string, myCart?: MyCartQuery['myCart']) { - if (!myCart) return 0; +function getQuantityInMyCart(productId: string, myCart?: MyCartQuery['myCart'], myChest?: MyChestQuery['chest']) { const cartItem = myCart?.cartItems.find((p) => p.id === productId); - if (!cartItem) return 0; - const quantities = cartItem.inventory.map((i) => i.quantity); - return quantities.reduce((a, b) => a + b, 0); + const chestItems = myChest?.items.filter((p) => p.productId === productId); + let quantity = 0; + if (cartItem) { + const quantities = cartItem.inventory.map((i) => i.quantity); + quantity += quantities.reduce((a, b) => a + b, 0); + } + quantity += chestItems.length; + return quantity; } // time diff in milliseconds @@ -54,11 +60,15 @@ const msToTime = (duration: number) => { export default function Product({ product }: { product: ProductsQuery['products'][number] }) { const { t } = useTranslation(); + const { user } = useUser(); const { showMessage } = useSnackbar(); const [selectedVariant, setSelectedVariant] = useState(product.inventory[0]); const [timeLeft, setTimeLeft] = useState(Number.MAX_VALUE); const { refetch: refetchMyCart, data } = useMyCartQuery(); - const quantityInMyCart = getQuantityInMyCart(product.id, data?.myCart); + const { data: chestData } = useMyChestQuery({ + variables: { studentId: user?.student_id }, + }); + const quantityInMyCart = getQuantityInMyCart(product.id, data?.myCart, chestData?.chest); const { refetch: refetchProducts } = useProductsQuery( { variables: { categoryId: product.category.id } }, ); diff --git a/frontend/generated/graphql.tsx b/frontend/generated/graphql.tsx index 62d356224..cee5bf09b 100644 --- a/frontend/generated/graphql.tsx +++ b/frontend/generated/graphql.tsx @@ -1775,6 +1775,7 @@ export type UserInventoryItem = { name: Scalars['String']; paidAt: Scalars['Date']; paidPrice: Scalars['Float']; + productId?: Maybe; variant?: Maybe; }; @@ -2068,7 +2069,7 @@ export type MyChestQueryVariables = Exact<{ }>; -export type MyChestQuery = { __typename?: 'Query', chest?: { __typename?: 'UserInventory', id: any, items: Array<{ __typename?: 'UserInventoryItem', id: any, name: string, description: string, paidPrice: number, imageUrl: string, variant?: string | null, paidAt: any, consumedAt?: any | null, category?: { __typename?: 'ProductCategory', id: any, name: string, description: string } | null } | null> } | null }; +export type MyChestQuery = { __typename?: 'Query', chest?: { __typename?: 'UserInventory', id: any, items: Array<{ __typename?: 'UserInventoryItem', id: any, name: string, description: string, paidPrice: number, imageUrl: string, variant?: string | null, paidAt: any, consumedAt?: any | null, productId?: any | null, category?: { __typename?: 'ProductCategory', id: any, name: string, description: string } | null } | null> } | null }; export type ConsumeItemMutationVariables = Exact<{ itemId: Scalars['UUID']; @@ -4095,6 +4096,7 @@ export const MyChestDocument = gql` } paidAt consumedAt + productId } } } From 236a78d07d397fe8c919e033850fff95766c624b Mon Sep 17 00:00:00 2001 From: Oliver Levay Date: Tue, 22 Aug 2023 18:34:56 +0200 Subject: [PATCH 2/2] :bug: Add Bite sponsorship and fix bug where removed products are still shown --- .../core/src/datasources/WebshopAPI.ts | 2 +- frontend/components/Nolla/locales/en.tsx | 3 + frontend/components/Nolla/locales/sv.tsx | 3 + frontend/package-lock.json | 231 ++++++++++++++++++ frontend/pages/nolla/index.tsx | 35 ++- 5 files changed, 272 insertions(+), 2 deletions(-) diff --git a/backend/services/core/src/datasources/WebshopAPI.ts b/backend/services/core/src/datasources/WebshopAPI.ts index f2212952c..37e457c19 100644 --- a/backend/services/core/src/datasources/WebshopAPI.ts +++ b/backend/services/core/src/datasources/WebshopAPI.ts @@ -105,7 +105,7 @@ export default class WebshopAPI extends dbUtils.KnexDataSource { getProducts(ctx: context.UserContext, categoryId?: string): Promise { return this.withAccess('webshop:read', ctx, async () => { - let query = this.knex(TABLE.PRODUCT); + let query = this.knex(TABLE.PRODUCT).whereNull('deleted_at'); if (categoryId) { query = query.where({ category_id: categoryId }); // not tested } diff --git a/frontend/components/Nolla/locales/en.tsx b/frontend/components/Nolla/locales/en.tsx index 4f1c954dd..541567d57 100644 --- a/frontend/components/Nolla/locales/en.tsx +++ b/frontend/components/Nolla/locales/en.tsx @@ -31,6 +31,9 @@ export default { map: 'Map', }, index: { + bite: { + copy: 'Beställ mat med gratis leverans i en månad med koden', + }, hero: { title: ( <> diff --git a/frontend/components/Nolla/locales/sv.tsx b/frontend/components/Nolla/locales/sv.tsx index 6a60ec8b7..0d29952db 100644 --- a/frontend/components/Nolla/locales/sv.tsx +++ b/frontend/components/Nolla/locales/sv.tsx @@ -31,6 +31,9 @@ export default { map: 'Karta', }, index: { + bite: { + copy: 'Beställ mat med gratis leverans i en månad med koden', + }, hero: { title: ( <> diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ea68c1d60..350cf0224 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11164,6 +11164,171 @@ "dependencies": { "zen-observable": "0.8.15" } + }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.3.tgz", + "integrity": "sha512-5O/ZIX6hlIRGMy1R2f/8WiCZ4Hp4WTC0FcTuz8ycQ28j/mzDnmzjVoayVVr+ZmfEKQayFrRu+vxHjFyY0JGQlQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.3.tgz", + "integrity": "sha512-2QWreRmlxYRDtnLYn+BI8oukHwcP7W0zGIY5R2mEXRjI4ARqCLdu8RmcT9Vemw7RfeAVKA/4cv/9PY0pCcQpNA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.3.tgz", + "integrity": "sha512-GtZdDLerM+VToCMFp+W+WhnT6sxHePQH4xZZiYD/Y8KFiwHbDRcJr2FPG0bAJnGNiSvv/QQnBq74wjZ9+7vhcQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.3.tgz", + "integrity": "sha512-gRYvTKrRYynjFQUDJ+upHMcBiNz0ii0m7zGgmUTlTSmrBWqVSzx79EHYT7Nn4GWHM+a/W+2VXfu+lqHcJeQ9gQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.3.tgz", + "integrity": "sha512-r+GLATzCjjQI82bgrIPXWEYBwZonSO64OThk5wU6HduZlDYTEDxZsFNoNoesCDWCgRrgg+OXj7WLNy1WlvfX7w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.3.tgz", + "integrity": "sha512-juvRj1QX9jmQScL4nV0rROtYUFgWP76zfdn1fdfZ2BhvwUugIAq8x+jLVGlnXKUhDrP9+RrAufqXjjVkK+uBxA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.3.tgz", + "integrity": "sha512-hzinybStPB+SzS68hR5rzOngOH7Yd/jFuWGeg9qS5WifYXHpqwGH2BQeKpjVV0iJuyO9r309JKrRWMrbfhnuBA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.3.tgz", + "integrity": "sha512-oyfQYljCwf+9zUu1YkTZbRbyxmcHzvJPMGOxC3kJOReh3kCUoGcmvAxUPMtFD6FSYjJ+eaog4+2IFHtYuAw/bQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.3.tgz", + "integrity": "sha512-FbnT3reJ3MbTJ5W0hvlCCGGVDSpburzT5XGC9ljBJ4kr+85iNTLjv7+vrPeDdwHEqtGmdZgnabkLVCI4yFyCag==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.3.tgz", + "integrity": "sha512-M/fKZC2tMGWA6eTsIniNEBpx2prdR8lIxvSO3gv5P6ymZOGVWCvEMksnTkPAjHnU6d8r8eCiuGKm3UNo7zCTpQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.3.tgz", + "integrity": "sha512-Ku9mfGwmNtk44o4B/jEWUxBAT4tJ3S7QbBMLJdL1GmtRZ05LGL36OqWjLvBPr8dFiHOQQbYoAmYfQw7zeGypYA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } }, "dependencies": { @@ -19259,6 +19424,72 @@ "requires": { "zen-observable": "0.8.15" } + }, + "@next/swc-android-arm-eabi": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.3.tgz", + "integrity": "sha512-5O/ZIX6hlIRGMy1R2f/8WiCZ4Hp4WTC0FcTuz8ycQ28j/mzDnmzjVoayVVr+ZmfEKQayFrRu+vxHjFyY0JGQlQ==", + "optional": true + }, + "@next/swc-android-arm64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.3.tgz", + "integrity": "sha512-2QWreRmlxYRDtnLYn+BI8oukHwcP7W0zGIY5R2mEXRjI4ARqCLdu8RmcT9Vemw7RfeAVKA/4cv/9PY0pCcQpNA==", + "optional": true + }, + "@next/swc-darwin-arm64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.3.tgz", + "integrity": "sha512-GtZdDLerM+VToCMFp+W+WhnT6sxHePQH4xZZiYD/Y8KFiwHbDRcJr2FPG0bAJnGNiSvv/QQnBq74wjZ9+7vhcQ==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.3.tgz", + "integrity": "sha512-gRYvTKrRYynjFQUDJ+upHMcBiNz0ii0m7zGgmUTlTSmrBWqVSzx79EHYT7Nn4GWHM+a/W+2VXfu+lqHcJeQ9gQ==", + "optional": true + }, + "@next/swc-freebsd-x64": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.3.tgz", + "integrity": "sha512-r+GLATzCjjQI82bgrIPXWEYBwZonSO64OThk5wU6HduZlDYTEDxZsFNoNoesCDWCgRrgg+OXj7WLNy1WlvfX7w==", + "optional": true + }, + "@next/swc-linux-arm-gnueabihf": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.3.tgz", + "integrity": "sha512-juvRj1QX9jmQScL4nV0rROtYUFgWP76zfdn1fdfZ2BhvwUugIAq8x+jLVGlnXKUhDrP9+RrAufqXjjVkK+uBxA==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.3.tgz", + "integrity": "sha512-hzinybStPB+SzS68hR5rzOngOH7Yd/jFuWGeg9qS5WifYXHpqwGH2BQeKpjVV0iJuyO9r309JKrRWMrbfhnuBA==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.3.tgz", + "integrity": "sha512-oyfQYljCwf+9zUu1YkTZbRbyxmcHzvJPMGOxC3kJOReh3kCUoGcmvAxUPMtFD6FSYjJ+eaog4+2IFHtYuAw/bQ==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.3.tgz", + "integrity": "sha512-FbnT3reJ3MbTJ5W0hvlCCGGVDSpburzT5XGC9ljBJ4kr+85iNTLjv7+vrPeDdwHEqtGmdZgnabkLVCI4yFyCag==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.3.tgz", + "integrity": "sha512-M/fKZC2tMGWA6eTsIniNEBpx2prdR8lIxvSO3gv5P6ymZOGVWCvEMksnTkPAjHnU6d8r8eCiuGKm3UNo7zCTpQ==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.3.tgz", + "integrity": "sha512-Ku9mfGwmNtk44o4B/jEWUxBAT4tJ3S7QbBMLJdL1GmtRZ05LGL36OqWjLvBPr8dFiHOQQbYoAmYfQw7zeGypYA==", + "optional": true } } } diff --git a/frontend/pages/nolla/index.tsx b/frontend/pages/nolla/index.tsx index 91e037abc..cba5bf29c 100644 --- a/frontend/pages/nolla/index.tsx +++ b/frontend/pages/nolla/index.tsx @@ -1,4 +1,6 @@ -import { Box, Button, Typography } from '@mui/material'; +import { + Box, Button, Typography, Stack, +} from '@mui/material'; import Image from 'next/image'; import { useRouter } from 'next/router'; import React from 'react'; @@ -6,6 +8,7 @@ import theme from '~/components/Nolla/theme'; import genGetProps from '~/functions/genGetServerSideProps'; import NollaLayout, { useNavItems } from '../../components/Nolla/layout'; import useNollaTranslate from '~/components/Nolla/useNollaTranslate'; +import Link from '~/components/Link'; export const getStaticProps = genGetProps(['nolla']); @@ -142,12 +145,42 @@ function Letter() { ); } +function Bite() { + const translate = useNollaTranslate(); + return ( + + + + {translate('index.bite.copy')} + {' '} + D2023 + + + + + ); +} + function LandingPage() { return ( <> + ); }