diff --git a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx index 0d651c26ab842d1ed7efcdae2955a954eeb234a1..107ea41dbf801ad30a39359647885622f26b73c4 100644 --- a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx +++ b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx @@ -33,7 +33,7 @@ import { useTranslation } from '@/blog/i18n/client'; import { postContainerClasses } from '@/blog/lib/post-layout-classes'; import sorter, { SortOrder } from '@/blog/lib/sorter'; import { DEFAULT_OBSERVER } from '@/blog/lib/utils'; -import { getBasePath } from '@/blog/utils/PathUtils'; +import { getBasePath } from '@ui/lib/path-utils'; import { useQuery } from '@tanstack/react-query'; import { getCommunity, getDiscussion, getListCommunityRoles, getPost } from '@transaction/lib/bridge-api'; import { Entry } from '@transaction/lib/extended-hive.chain'; diff --git a/apps/blog/features/layouts/communities-select.tsx b/apps/blog/features/layouts/communities-select.tsx index 5d4c953c66d0d29b0f83c3dc9b01a12b9e3efb03..c90086b1383fa00783bb499ffdb57e0f5beaa371 100644 --- a/apps/blog/features/layouts/communities-select.tsx +++ b/apps/blog/features/layouts/communities-select.tsx @@ -12,7 +12,7 @@ import { } from '@ui/components/select'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from '@/blog/i18n/client'; -import { withBasePath } from '../../utils/PathUtils'; +import { withBasePath } from '@ui/lib/path-utils'; import { getCommunities, getSubscriptions } from '@transaction/lib/bridge-api'; import { useRouter } from 'next/navigation'; import { useUserClient } from '@smart-signer/lib/auth/use-user-client'; diff --git a/apps/blog/features/layouts/site-header/client-effects.tsx b/apps/blog/features/layouts/site-header/client-effects.tsx index a6ae8a7fb89a5122b9e687d79c719899abde58db..6061b4adb2a93c0a3362f324c50d4618440c6060 100644 --- a/apps/blog/features/layouts/site-header/client-effects.tsx +++ b/apps/blog/features/layouts/site-header/client-effects.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect } from 'react'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import { getLanguage } from '@/blog/utils/language'; export default function ClientEffects() { diff --git a/apps/blog/features/post-editor/post-form.tsx b/apps/blog/features/post-editor/post-form.tsx index d659270dc839da669524f9c12a07d87469e71b72..bfd50b03263cbdd2859cec33cea336ca0302963d 100644 --- a/apps/blog/features/post-editor/post-form.tsx +++ b/apps/blog/features/post-editor/post-form.tsx @@ -23,7 +23,7 @@ import { useQuery } from '@tanstack/react-query'; import { Entry } from '@transaction/lib/extended-hive.chain'; import { getCommunity, getSubscriptions } from '@transaction/lib/bridge-api'; import { Icons } from '@ui/components/icons'; -import { withBasePath } from '@/blog/utils/PathUtils'; +import { withBasePath } from '@ui/lib/path-utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@ui/components/tooltip'; import { debounce, DEFAULT_OBSERVER, DEFAULT_PREFERENCES, Preferences } from '@/blog/lib/utils'; import { getLogger } from '@ui/lib/logging'; diff --git a/apps/blog/middleware.ts b/apps/blog/middleware.ts index 1aad2ad47fcd3a5565e098275fb7a7c0b8caf0ab..2239e7df1f3861acef03af08d2137afc36520a9d 100644 --- a/apps/blog/middleware.ts +++ b/apps/blog/middleware.ts @@ -1,5 +1,4 @@ -import { type NextRequest, NextResponse } from 'next/server'; -import { setLoginChallengeCookies } from '@hive/smart-signer/lib/middleware-challenge-cookies'; +import { createMiddleware } from '@hive/middleware/lib/common'; // NOTE: Nonce-based CSP is disabled because Next.js 14 doesn't fully support it. // Next.js internal scripts (__NEXT_DATA__, hydration) don't receive nonces automatically, @@ -7,22 +6,7 @@ import { setLoginChallengeCookies } from '@hive/smart-signer/lib/middleware-chal // The static CSP in next.config.js provides protection without causing violations. // See GitLab issue #796 for tracking nonce CSP support in future Next.js versions. -export async function middleware(request: NextRequest): Promise { - const { pathname } = request.nextUrl; - const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; - - const res = NextResponse.next(); - - // Set login challenge cookies (needed for console logging process) - setLoginChallengeCookies(request, res); - - // CSP is now set via next.config.js headers() - no middleware override needed - // The static CSP uses 'unsafe-inline' which is compatible with Next.js - - // In blog, redirect root path to /trending - if (pathname === '/' || pathname === `${basePath}` || pathname === `${basePath}/`) { - return NextResponse.redirect(new URL(`${basePath}/trending`, request.url), { status: 302 }); - } - - return res; -} +// Blog-specific middleware: redirects root to /trending +export const middleware = createMiddleware({ + rootRedirect: '/trending' +}); diff --git a/apps/blog/pages/interaction/[uid].tsx b/apps/blog/pages/interaction/[uid].tsx index deb76787619af53a7eaeca45047da299bf85323b..039392a1e698e03279c7c6384276e49f8bc1d9a6 100644 --- a/apps/blog/pages/interaction/[uid].tsx +++ b/apps/blog/pages/interaction/[uid].tsx @@ -2,7 +2,7 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; import { getLogger } from '@ui/lib/logging'; import { siteConfig } from '@ui/config/site'; -import { withBasePath } from '@/blog/utils/PathUtils'; +import { withBasePath } from '@ui/lib/path-utils'; const logger = getLogger('app'); diff --git a/apps/blog/utils/language.ts b/apps/blog/utils/language.ts index eb62babf0c45b95af8a574a14d8157e9303f328a..6a0faefe8e724107a58cb05d0e643d06fffe8beb 100644 --- a/apps/blog/utils/language.ts +++ b/apps/blog/utils/language.ts @@ -1,3 +1,5 @@ +import { getCookie } from '@ui/lib/utils'; + export const LOCALE_KEY = 'NEXT_LOCALE'; export const getLanguage = () => { @@ -9,11 +11,3 @@ export const setLanguage = (locale: string) => { document.cookie = `${LOCALE_KEY}=${locale}; SameSite=Lax; path=/`; localStorage.setItem(LOCALE_KEY, locale); }; - -export const getCookie = (name: string) => { - if (typeof document === 'undefined') return null; - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop()?.split(';').shift(); - return null; -}; diff --git a/apps/wallet/components/lang-toggle.tsx b/apps/wallet/components/lang-toggle.tsx index f54004fc0c89c7f2edcaef9a0c2afca3755907e1..39b5ffeff10bc5c5ec00e326d0fe03172dc7b367 100644 --- a/apps/wallet/components/lang-toggle.tsx +++ b/apps/wallet/components/lang-toggle.tsx @@ -8,7 +8,7 @@ import { } from '@ui/components/dropdown-menu'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import clsx from 'clsx'; import { useTranslation } from 'next-i18next'; import TooltipContainer from '@ui/components/tooltip-container'; diff --git a/apps/wallet/components/mobile-nav.tsx b/apps/wallet/components/mobile-nav.tsx index f8b546e34c4eb8e3993753c838c8abfde597f177..ba240d1268e34babd111d553fcc60412b528013c 100644 --- a/apps/wallet/components/mobile-nav.tsx +++ b/apps/wallet/components/mobile-nav.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import { SidebarOpen } from 'lucide-react'; import { siteConfig } from '@ui/config/site'; import { cn } from '@ui/lib/utils'; -import { withBasePath } from '@/wallet/utils/PathUtils'; +import { withBasePath } from '@ui/lib/path-utils'; import { Button } from '@ui/components/button'; import { Sheet, SheetContent, SheetTrigger } from '@ui/components/sheet'; import { Icons } from '@ui/components/icons'; diff --git a/apps/wallet/lib/get-translations.ts b/apps/wallet/lib/get-translations.ts index b1fc2890b0f57fb5142c23acbe10662c888715d2..a5637304c96eb6e9fd827832b29158e551e13e54 100644 --- a/apps/wallet/lib/get-translations.ts +++ b/apps/wallet/lib/get-translations.ts @@ -1,7 +1,6 @@ import { GetStaticPropsContext, GetServerSidePropsContext } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { i18n } from '@/wallet/next-i18next.config'; -import { getAccountFull } from '@transaction/lib/hive-api'; // Unified getTranslations function supporting both SSR and SSG export const getTranslations = async ( @@ -31,49 +30,3 @@ export const getServerSidePropsDefault = async (ctx: GetStaticPropsContext | Get } }; }; - -export interface MetadataProps { - tabTitle: string; - description: string; - image: string; - title: string; -} - -export const getAccountMetadata = async ( - firstParam: string, - descriptionText: string -): Promise => { - let metadata = { - tabTitle: '', - description: '', - image: '', - title: firstParam - }; - if (firstParam.startsWith('@')) { - try { - const username = firstParam.split('@')[1]; - const data = await getAccountFull(username); - - if (!data) { - throw new Error(`Account ${username} not found`); - } - - const displayName = data.profile?.name || data.name; - const defaultImage = 'https://hive.blog/images/hive-blog-share.png'; - - metadata = { - ...metadata, - image: data.profile?.profile_image || defaultImage, - tabTitle: - displayName === username - ? `${descriptionText} ${displayName} - Hive` - : `${descriptionText} ${displayName} (${firstParam}) - Hive`, - description: - data.profile?.about || `${descriptionText} ${firstParam}. Hive: Communities Without Borders.` - }; - } catch (error) { - console.error('Error fetching account:', error); - } - } - return metadata; -}; diff --git a/apps/wallet/pages/[param]/author-rewards.tsx b/apps/wallet/pages/[param]/author-rewards.tsx index d0908ade79623f0baa03c45ab17b99de637db333..9455592c5c05b7f192438fddbf659a831fb97427 100644 --- a/apps/wallet/pages/[param]/author-rewards.tsx +++ b/apps/wallet/pages/[param]/author-rewards.tsx @@ -1,7 +1,8 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import ProfileLayout from '@/wallet/components/common/profile-layout'; import { useTranslation } from 'next-i18next'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import Head from 'next/head'; import { useRewardsHistory } from '@/wallet/components/hooks/use-rewards-history'; import { Link } from '@hive/ui'; diff --git a/apps/wallet/pages/[param]/authorities.tsx b/apps/wallet/pages/[param]/authorities.tsx index 5d8f26a915e68fd665886e077db247411d755961..b899228d7d239218a889788ced006e110e978c5a 100644 --- a/apps/wallet/pages/[param]/authorities.tsx +++ b/apps/wallet/pages/[param]/authorities.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import ProfileLayout from '@/wallet/components/common/profile-layout'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { useTranslation } from 'next-i18next'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import WalletMenu from '@/wallet/components/wallet-menu'; import Loading from '@ui/components/loading'; import { useUser } from '@smart-signer/lib/auth/use-user'; diff --git a/apps/wallet/pages/[param]/communities.tsx b/apps/wallet/pages/[param]/communities.tsx index f5f49c36d1235bdc7896663cb3454ff76f155791..86718070037e0c89c35a500a784631184052a277 100644 --- a/apps/wallet/pages/[param]/communities.tsx +++ b/apps/wallet/pages/[param]/communities.tsx @@ -21,7 +21,8 @@ import { Textarea } from '@ui/components'; import { useEffect, useState } from 'react'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import { ESupportedLanguages } from '@hiveio/wax'; import { useCreateCommunityMutation } from '@/wallet/components/hooks/use-create-community-mutation'; import { z } from 'zod'; diff --git a/apps/wallet/pages/[param]/curation-rewards.tsx b/apps/wallet/pages/[param]/curation-rewards.tsx index 964d5c2f1fd45e810bdd4ae27ab7a0f1fbd4384c..214def15f6f4f28f425f88c80a9ccd13cc98caca 100644 --- a/apps/wallet/pages/[param]/curation-rewards.tsx +++ b/apps/wallet/pages/[param]/curation-rewards.tsx @@ -1,7 +1,8 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import ProfileLayout from '@/wallet/components/common/profile-layout'; import { useTranslation } from 'next-i18next'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import Head from 'next/head'; import { useRewardsHistory } from '@/wallet/components/hooks/use-rewards-history'; import Loading from '@ui/components/loading'; diff --git a/apps/wallet/pages/[param]/delegations.tsx b/apps/wallet/pages/[param]/delegations.tsx index 8fe824272347823f8d6f05334d6de1a133214486..512f7f8249c1c164e97f9d467a4814f43df15b08 100644 --- a/apps/wallet/pages/[param]/delegations.tsx +++ b/apps/wallet/pages/[param]/delegations.tsx @@ -7,7 +7,8 @@ import Loading from '@ui/components/loading'; import ProfileLayout from '@/wallet/components/common/profile-layout'; import { useTranslation } from 'next-i18next'; import WalletMenu from '@/wallet/components/wallet-menu'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import RevokeDialog from '@/wallet/components/revoke-dialog'; import { useUser } from '@smart-signer/lib/auth/use-user'; import Head from 'next/head'; diff --git a/apps/wallet/pages/[param]/password.tsx b/apps/wallet/pages/[param]/password.tsx index 1eebedc086169773aac20a439f2d8cf108f3f912..05891b0bea66b871bc1408e39e910f6b2d108849 100644 --- a/apps/wallet/pages/[param]/password.tsx +++ b/apps/wallet/pages/[param]/password.tsx @@ -12,7 +12,8 @@ import { useTranslation } from 'next-i18next'; import { useSiteParams } from '@ui/components/hooks/use-site-params'; import ProfileLayout from '@/wallet/components/common/profile-layout'; import WalletMenu from '@/wallet/components/wallet-menu'; -import { getAccountMetadata, getTranslations, MetadataProps } from '@/wallet/lib/get-translations'; +import { getAccountMetadata, MetadataProps } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import { useChangePasswordMutation } from '@/wallet/components/hooks/use-change-password-mutation'; import { handleError } from '@ui/lib/handle-error'; import { Icons } from '@ui/components/icons'; diff --git a/apps/wallet/pages/[param]/permissions.tsx b/apps/wallet/pages/[param]/permissions.tsx index 0aa520997d4e64e68c6e5ac4579ef06faf18c0aa..88445da37cbe660f51a8614fa051c5bd25299cae 100644 --- a/apps/wallet/pages/[param]/permissions.tsx +++ b/apps/wallet/pages/[param]/permissions.tsx @@ -4,7 +4,8 @@ import WalletMenu from '@/wallet/components/wallet-menu'; import { Card, Separator } from '@ui/components'; import { Link } from '@hive/ui'; import { cn } from '@ui/lib/utils'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import { useTranslation } from 'next-i18next'; import { useUser } from '@smart-signer/lib/auth/use-user'; import env from '@beam-australia/react-env'; diff --git a/apps/wallet/pages/[param]/transfers.tsx b/apps/wallet/pages/[param]/transfers.tsx index e17b9e13a706252c9cf6907b8e178889a1f74403..20a334d9c712a5c0af50ce23ca4d8bc89f279f28 100644 --- a/apps/wallet/pages/[param]/transfers.tsx +++ b/apps/wallet/pages/[param]/transfers.tsx @@ -47,7 +47,8 @@ import { import { useUser } from '@smart-signer/lib/auth/use-user'; import { TransferDialog } from '@/wallet/components/transfer-dialog'; import useFilters from '@/wallet/components/hooks/use-filters'; -import { getAccountMetadata, getTranslations } from '@/wallet/lib/get-translations'; +import { getAccountMetadata } from '@transaction/lib/metadata'; +import { getTranslations } from '@/wallet/lib/get-translations'; import FinancialReport from '@/wallet/components/financial-report'; import { useClaimRewardsMutation } from '@/wallet/components/hooks/use-claim-rewards-mutation'; import { useMemo, useState } from 'react'; diff --git a/apps/wallet/pages/_app.tsx b/apps/wallet/pages/_app.tsx index c523ab43963b8c9760b213c63da332f0c61f5388..7955e4ba3d694958fa0f7de8001d62c8a692975c 100644 --- a/apps/wallet/pages/_app.tsx +++ b/apps/wallet/pages/_app.tsx @@ -2,7 +2,7 @@ import '@hive/tailwindcss-config/globals.css'; import type { AppProps } from 'next/app'; import { lazy, Suspense, useEffect, useLayoutEffect } from 'react'; import { appWithTranslation } from 'next-i18next'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import { i18n } from 'next-i18next.config'; import i18nConfig from '../next-i18next.config'; diff --git a/apps/wallet/utils/PathUtils.ts b/apps/wallet/utils/PathUtils.ts deleted file mode 100644 index e8655386a9a27677e000a8c68fb97dd4b81fa9dd..0000000000000000000000000000000000000000 --- a/apps/wallet/utils/PathUtils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import env from '@beam-australia/react-env'; - -/** - * Prepends the configured base path to a given path - * Gets basePath from runtime environment to match Next.js configuration - * @param path - The path to prepend the base path to - * @returns The path with base path prepended - */ -export function withBasePath(path: string): string { - // Get basePath from environment - const basePath = (typeof window !== 'undefined') - ? env('BASE_PATH') || '' - : process.env.NEXT_PUBLIC_BASE_PATH || ''; - - // If no base path or path is already absolute URL, return as-is - if (!basePath || path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) { - return path; - } - - // Handle hash and query parameters - if (path.startsWith('#') || path.startsWith('?')) { - return path; - } - - // Ensure path starts with / - const normalizedPath = path.startsWith('/') ? path : `/${path}`; - - // Combine base path with path, avoiding double slashes - return `${basePath}${normalizedPath}`; -} - -/** - * Gets the current base path - * @returns The configured base path or empty string - */ -export function getBasePath(): string { - // In server/build time, get from process.env - if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASE_PATH) { - return process.env.NEXT_PUBLIC_BASE_PATH; - } - - // In browser, get from env() if available - const basePath = (typeof window !== 'undefined') - ? env('BASE_PATH') || '' - : ''; - return basePath; -} - -/** - * Prepends base path to image src for Next.js Image component - * @param src - The image source path - * @returns The src with base path prepended if needed - */ -export function getImageSrc(src: string): string { - // If already an external URL, return as-is - if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) { - return src; - } - - const basePath = getBasePath(); - - // If no base path or src already includes it, return as-is - if (!basePath || src.startsWith(basePath)) { - return src; - } - - // Prepend base path - return `${basePath}${src.startsWith('/') ? src : '/' + src}`; -} - -/** - * Removes the base path from a given path if present - * Useful for extracting the actual route from a full path - * @param path - The path to remove the base path from - * @returns The path without base path - */ -export function removeBasePath(path: string): string { - const basePath = getBasePath(); - - if (!basePath || !path.startsWith(basePath)) { - return path; - } - - const pathWithoutBase = path.slice(basePath.length); - return pathWithoutBase.startsWith('/') ? pathWithoutBase : '/' + pathWithoutBase; -} \ No newline at end of file diff --git a/packages/logger/index.ts b/packages/logger/index.ts index ba88c7e4930449ff4b3072b5a36549ba938ece7d..ec7722fead0d88aec7c5a00bb428f2ea8c117d79 100644 --- a/packages/logger/index.ts +++ b/packages/logger/index.ts @@ -1,114 +1,9 @@ -import pino, { Logger } from 'pino'; -import env from '@beam-australia/react-env'; - -export const logLevelData = { - '*': env('LOGGING_LOG_LEVEL') ? env('LOGGING_LOG_LEVEL').toLowerCase() : 'info' -}; - -export const logLevels = new Map(Object.entries(logLevelData)); - -export function getLogLevel(logger: string): string { - return logLevels.get(logger) || logLevels.get('*') || 'info'; -} - -/** - * Get instance of pino logger. - * - * Use this way: - * ``` - * import { getLogger } from "@hive/logger"; - * const logger = getLogger('app'); - * logger.info("an info message from _app"); - * logger.info({username: 'John', id: 2}, "another info message from _app"); - * ``` - * - * See https://github.com/pinojs/pino/blob/master/docs/api.md. - * See https://betterstack.com/community/guides/logging/how-to-install-setup-and-use-pino-to-log-node-js-applications/ - * - * @export - * @param {string} name - * @returns {Logger} - */ -export function getLogger(name: string): Logger { - return pino({ - name, - level: getLogLevel(name), - formatters: { - level: (label: string) => { - return { level: label.toUpperCase() }; - } - }, - timestamp: pino.stdTimeFunctions.isoTime, - browser: { - disabled: env('LOGGING_BROWSER_ENABLED') - ? env('LOGGING_BROWSER_ENABLED').toLowerCase() === 'true' - ? false - : true - : true, - asObject: false - } - }); -} - - -export interface LoggerLogLevels { - off: number, - fatal: number, - error: number, - warn: number, - info: number, - debug: number, - trace: number, - all: number, -}; - -export type LoggerOutput = 'console' | 'noop'; - /** - * Utility function for testing whether we are in browser. - * + * @deprecated This package is deprecated. + * Use `import { getLogger } from '@ui/lib/logging'` for logging. + * Use `import { isBrowser, loggerStyles } from '@ui/lib/logger'` for other utilities. + * + * This file is kept for potential edge cases but should not be imported directly. */ -export const isBrowser = () => typeof window !== 'undefined' && window; -// For fancy log messages. -const commonStyle = [ - 'padding: 1px; padding-right: 4px; font-family: "Helvetica";', - 'border-left: 3px solid #0a2722;', -]; -export const loggerStyles = { - slimConstructor: [ - 'color: white;', - 'background-color: #276156;', - ...commonStyle, - ].join(' '), - slimDestructor: [ - 'color: black;', - 'background-color: rgb(255, 180, 0);', - ...commonStyle, - ].join(' '), - slimFatal: [ - 'color: white;', - 'background-color: rgb(0, 0, 0);', - ...commonStyle, - ].join(' '), - slimError: [ - 'color: white;', - 'background-color: rgb(255, 80, 80);', - ...commonStyle, - ].join(' '), - slimWarn: [ - 'color: black;', - 'background-color: rgb(255, 255, 153);', - ...commonStyle, - ].join(' '), - slimInfo: [ - 'color: white;', - 'background-color: rgb(55,105,150);', - ...commonStyle, - ].join(' '), - slimDebug: [ - 'color: black;', - 'background-color: rgb(153, 255, 204);', - ...commonStyle, - ].join(' '), -}; +export {}; \ No newline at end of file diff --git a/packages/logger/package.json b/packages/logger/package.json index 1fe9dec4086efa817c991456fd0838e57eca97b5..fec0fd22f2281a4247aab4aa5dba4f0bc5c62238 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -9,6 +9,7 @@ "dependencies": { "@beam-australia/react-env": "^3.1.1", "@hive/eslint-config-custom": "workspace:*", + "@hive/ui": "workspace:*", "pino": "^8.17.1" } } \ No newline at end of file diff --git a/packages/middleware/lib/common.ts b/packages/middleware/lib/common.ts index db97c536701ede40922f05dbd67a68acc619e6bc..bcd9cb1e5fbf89c142071e74e54f6e1da7dfc584 100644 --- a/packages/middleware/lib/common.ts +++ b/packages/middleware/lib/common.ts @@ -6,26 +6,62 @@ import { logPageVisit } from './auth-proof-cookie'; const logger = getLogger('middleware'); -export async function commonMiddleware(request: NextRequest) { - const { pathname } = request.nextUrl; +/** + * Configuration options for the common middleware + */ +export interface MiddlewareConfig { + /** + * If provided, redirect root path (/) to this path + * Example: '/trending' will redirect / to /trending + */ + rootRedirect?: string; +} + +/** + * Creates a configured middleware function + * @param config - Optional configuration for app-specific behavior + */ +export function createMiddleware(config: MiddlewareConfig = {}) { + return async function middleware(request: NextRequest): Promise { + const { pathname } = request.nextUrl; + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; + + const res = NextResponse.next(); - const res = NextResponse.next(); + setLoginChallengeCookies(request, res); - setLoginChallengeCookies(request, res); + if (pathname.match('/((?!api|_next/static|_next/image|favicon.ico).*)')) { + const isPrefetch = + request.headers.get('x-middleware-prefetch') === '1' || + request.headers.get('purpose') === 'prefetch' || + request.headers.get('sec-purpose')?.includes('prefetch'); - if (pathname.match('/((?!api|_next/static|_next/image|favicon.ico).*)')) { - const isPrefetch = - request.headers.get('x-middleware-prefetch') === '1' || - request.headers.get('purpose') === 'prefetch' || - request.headers.get('sec-purpose')?.includes('prefetch'); + if (!isPrefetch) { + // Log page visits for authenticated users (if they have auth proof cookie) + logPageVisit(request, pathname); + } + } - if (!isPrefetch) { - // Log page visits for authenticated users (if they have auth proof cookie) - logPageVisit(request, pathname); + // Handle root redirect if configured + if (config.rootRedirect) { + if (pathname === '/' || pathname === `${basePath}` || pathname === `${basePath}/`) { + return NextResponse.redirect( + new URL(`${basePath}${config.rootRedirect}`, request.url), + { status: 302 } + ); + } } - } - return res; + return res; + }; +} + +/** + * Default middleware without any app-specific configuration + * @deprecated Use createMiddleware() for new code + */ +export async function commonMiddleware(request: NextRequest) { + return createMiddleware()(request); } // export const config = { diff --git a/packages/smart-signer/components/auth/process.tsx b/packages/smart-signer/components/auth/process.tsx index 9b194bcdfe6b2d863e7bfe668622285fc24efd40..32264c30a4deb9920244f35ff60edc4577aadb00 100644 --- a/packages/smart-signer/components/auth/process.tsx +++ b/packages/smart-signer/components/auth/process.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, MutableRefObject } from 'react'; import { cookieNamePrefix } from '@smart-signer/lib/session'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import { KeyType } from '@smart-signer/types/common'; import { useSignIn } from '@smart-signer/lib/auth/use-sign-in'; import { Signatures, PostLoginSchema } from '@smart-signer/lib/auth/utils'; diff --git a/packages/smart-signer/components/login-panel.tsx b/packages/smart-signer/components/login-panel.tsx index 9e60f707709471459eb2744b1c176617d9cd36dc..c32178880099a02234ed5b871d5b3d36054c6872 100644 --- a/packages/smart-signer/components/login-panel.tsx +++ b/packages/smart-signer/components/login-panel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { LoginType } from '@smart-signer/types/common'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import { Signatures, PostLoginSchema } from '@smart-signer/lib/auth/utils'; import { useSignIn } from '@smart-signer/lib/auth/use-sign-in'; import { useUser } from '@smart-signer/lib/auth/use-user'; diff --git a/packages/smart-signer/components/signin-panel.tsx b/packages/smart-signer/components/signin-panel.tsx index f2cad607cc64a2ab56280bc526827eb5482f4e77..ec01c6c6759ed32564e8f071ef3fba4964dcb160 100644 --- a/packages/smart-signer/components/signin-panel.tsx +++ b/packages/smart-signer/components/signin-panel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { LoginType } from '@smart-signer/types/common'; -import { getCookie } from '@smart-signer/lib/utils'; +import { getCookie } from '@ui/lib/utils'; import { Signatures, PostLoginSchema } from '@smart-signer/lib/auth/utils'; import { useSignIn } from '@smart-signer/lib/auth/use-sign-in'; import { useUser } from '@smart-signer/lib/auth/use-user'; diff --git a/packages/smart-signer/lib/auth/use-user-client.ts b/packages/smart-signer/lib/auth/use-user-client.ts index d1ec7afb3edca883da5af6233db33a7e5c7b7b77..7887da1f592822f03c003932b4bac06bc34d1465 100644 --- a/packages/smart-signer/lib/auth/use-user-client.ts +++ b/packages/smart-signer/lib/auth/use-user-client.ts @@ -1,67 +1,23 @@ 'use client'; -import { useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { QUERY_KEY } from '@smart-signer/lib/query-keys'; -import * as userLocalStorage from './user-localstore'; -import { useIsMounted, useLocalStorage } from 'usehooks-ts'; -import { fetchJson } from '@smart-signer/lib/fetch-json'; -import { defaultUser } from '@smart-signer/lib/auth/utils'; -import { getLogger } from '@ui/lib/logging'; -import { User } from '@smart-signer/types/common'; - -const isServer = typeof window === 'undefined'; - -const logger = getLogger('app'); - -interface IUseUser { - user: User; -} - -async function getUser(): Promise { - return await fetchJson(`/api/users/me`); -} - -export function useUserClient({ redirectTo = '', redirectIfFound = false } = {}): IUseUser { +import { useIsMounted } from 'usehooks-ts'; +import { useUserCore, IUseUser, UseUserOptions } from './use-user-core'; + +/** + * User authentication hook for App Router (next/navigation). + * Use useUser for Pages Router components. + * + * @param options - Configuration options for redirects + * @returns User data + */ +export function useUserClient(options: UseUserOptions = {}): IUseUser { const isMounted = useIsMounted(); - const [storedUser, storeUser] = useLocalStorage('user', defaultUser); - const { data: user } = useQuery({ - queryKey: [QUERY_KEY.user], - queryFn: async (): Promise => getUser(), - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - initialData: storedUser, - onError: () => { - storeUser(defaultUser); - } - }); - const router = useRouter(); - useEffect(() => { - userLocalStorage.saveUser(user || defaultUser); - }, [user]); - - useEffect(() => { - // If no redirect needed, just return (example: already on - // /dashboard). If user data not yet there (fetch in progress, - // logged in or not) then don't do anything yet. - if (!redirectTo || !user) { - return; - } - - if ( - // If redirectTo is set, redirect if the user was not found. - (redirectTo && !redirectIfFound && !user?.isLoggedIn) || - // If redirectIfFound is also set, redirect if the user was found. - (redirectIfFound && user?.isLoggedIn) - ) { - router.push(redirectTo); - } - }, [user, redirectIfFound, redirectTo, router]); + const handleRedirect = useCallback((path: string) => { + router.push(path); + }, [router]); - return { - user: !isMounted() || !user ? defaultUser : user - }; + return useUserCore(options, handleRedirect, isMounted); } diff --git a/packages/smart-signer/lib/auth/use-user-core.ts b/packages/smart-signer/lib/auth/use-user-core.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fe720a9f6b7f1bb1adeef65d4489e86b81de2a3 --- /dev/null +++ b/packages/smart-signer/lib/auth/use-user-core.ts @@ -0,0 +1,82 @@ +import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { QUERY_KEY } from '@smart-signer/lib/query-keys'; +import * as userLocalStorage from './user-localstore'; +import { useLocalStorage } from 'usehooks-ts'; +import { fetchJson } from '@smart-signer/lib/fetch-json'; +import { defaultUser } from '@smart-signer/lib/auth/utils'; +import { getLogger } from '@ui/lib/logging'; +import { User } from '@smart-signer/types/common'; + +const logger = getLogger('app'); + +export interface IUseUser { + user: User; +} + +export interface UseUserOptions { + redirectTo?: string; + redirectIfFound?: boolean; +} + +async function getUser(): Promise { + return await fetchJson(`/api/users/me`); +} + +/** + * Core user hook logic shared between Pages Router and App Router versions. + * + * @param options - Configuration options + * @param onRedirect - Callback to handle redirects (router-specific) + * @param isMounted - Optional function to check if component is mounted (for App Router) + * @returns User data and query state + */ +export function useUserCore( + { redirectTo = '', redirectIfFound = false }: UseUserOptions = {}, + onRedirect: (path: string) => void, + isMounted?: () => boolean +): IUseUser { + const [storedUser, storeUser] = useLocalStorage('user', defaultUser); + const { data: user } = useQuery({ + queryKey: [QUERY_KEY.user], + queryFn: async (): Promise => getUser(), + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + initialData: storedUser, + onError: () => { + storeUser(defaultUser); + } + }); + + useEffect(() => { + userLocalStorage.saveUser(user || defaultUser); + }, [user]); + + useEffect(() => { + // If no redirect needed, just return (example: already on + // /dashboard). If user data not yet there (fetch in progress, + // logged in or not) then don't do anything yet. + if (!redirectTo || !user) { + return; + } + + if ( + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound && !user?.isLoggedIn) || + // If redirectIfFound is also set, redirect if the user was found. + (redirectIfFound && user?.isLoggedIn) + ) { + onRedirect(redirectTo); + } + }, [user, redirectIfFound, redirectTo, onRedirect]); + + // For App Router, check if mounted before returning user + const resolvedUser = isMounted + ? (!isMounted() || !user ? defaultUser : user) + : (user ?? defaultUser); + + return { + user: resolvedUser + }; +} diff --git a/packages/smart-signer/lib/auth/use-user.tsx b/packages/smart-signer/lib/auth/use-user.tsx index c505d7aa192ab1c305f80aefb3646b810a12c959..d6eb931749c94f59edad1eed81089fcc1af1be54 100644 --- a/packages/smart-signer/lib/auth/use-user.tsx +++ b/packages/smart-signer/lib/auth/use-user.tsx @@ -1,61 +1,18 @@ -import { useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; import Router from 'next/router'; -import { QUERY_KEY } from '@smart-signer/lib/query-keys'; -import * as userLocalStorage from './user-localstore'; -import { useLocalStorage } from 'usehooks-ts'; -import { fetchJson } from '@smart-signer/lib/fetch-json'; -import { defaultUser } from '@smart-signer/lib/auth/utils'; -import { getLogger } from '@ui/lib/logging'; -import { User } from '@smart-signer/types/common'; - -const logger = getLogger('app'); - -interface IUseUser { - user: User; -} - -async function getUser(): Promise { - return await fetchJson(`/api/users/me`); -} - -export function useUser({ redirectTo = '', redirectIfFound = false } = {}): IUseUser { - const [storedUser, storeUser] = useLocalStorage('user', defaultUser); - const { data: user } = useQuery({ - queryKey: [QUERY_KEY.user], - queryFn: async (): Promise => getUser(), - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - initialData: storedUser, - onError: () => { - storeUser(defaultUser); - } - }); - - useEffect(() => { - userLocalStorage.saveUser(user || defaultUser); - }, [user]); - - useEffect(() => { - // If no redirect needed, just return (example: already on - // /dashboard). If user data not yet there (fetch in progress, - // logged in or not) then don't do anything yet. - if (!redirectTo || !user) { - return; - } - - if ( - // If redirectTo is set, redirect if the user was not found. - (redirectTo && !redirectIfFound && !user?.isLoggedIn) || - // If redirectIfFound is also set, redirect if the user was found. - (redirectIfFound && user?.isLoggedIn) - ) { - Router.push(redirectTo); - } - }, [user, redirectIfFound, redirectTo]); - - return { - user: user ?? defaultUser - }; +import { useUserCore, IUseUser, UseUserOptions } from './use-user-core'; + +/** + * User authentication hook for Pages Router (next/router). + * Use useUserClient for App Router components. + * + * @param options - Configuration options for redirects + * @returns User data + */ +export function useUser(options: UseUserOptions = {}): IUseUser { + const handleRedirect = useCallback((path: string) => { + Router.push(path); + }, []); + + return useUserCore(options, handleRedirect); } diff --git a/packages/smart-signer/lib/utils.ts b/packages/smart-signer/lib/utils.ts index e4824759d4814c0595b7fbd69f80eac2a7cb31ff..266cc3a024beee9cdb994040d27d3a3738ba1694 100644 --- a/packages/smart-signer/lib/utils.ts +++ b/packages/smart-signer/lib/utils.ts @@ -4,69 +4,24 @@ const KEY_TYPES = ['active', 'posting'] as const; export type KeyAuthorityType = (typeof KEY_TYPES)[number]; /** - * Return cookie value for given cookie name. For use on client only. - * When cookie doesn't exist returns empty string. - * - * @export - * @param {string} cname - * @returns {string} + * Checks if the specified storage type is available in the browser. */ -export function getCookie(cname: string): string { - let name = cname + '='; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ''; -} - -export function isStorageAvailable( - storageType: 'localStorage' | 'sessionStorage', - strict: boolean = false // if true also tries to read and write to storage -) { - let storage: Storage; - // logger.info('Checking availability of %s', storageType); +export function isStorageAvailable(storageType: 'localStorage' | 'sessionStorage'): boolean { try { if (!isBrowser()) return false; - if (storageType === 'localStorage') { - storage = window.localStorage; - } else if (storageType === 'sessionStorage') { - storage = window.sessionStorage; - } else { - return false; - } - - // Disabled, because we experience too many writes here. - // TODO Check why. - // if (strict) { - // const x = '__storage_test__'; - // storage.setItem(x, x); - // storage.removeItem(x); - // } - - return true; - } catch (e) { + return storageType in window && window[storageType] !== null; + } catch { return false; } } /** - * Returns true if page is loaded in iframe, false otherwise. - * - * @export - * @returns {boolean} + * Returns true if the page is loaded in an iframe, false otherwise. */ export function inIframe(): boolean { try { return window.self !== window.top; - } catch (e) { + } catch { return true; } } diff --git a/packages/transaction/lib/app-types.ts b/packages/transaction/lib/app-types.ts index 367a149021ff991d7cf16165d4cd48f19c0964e0..bec6ae7dc32ebf7dc05effd209ce1bc39c594d8b 100644 --- a/packages/transaction/lib/app-types.ts +++ b/packages/transaction/lib/app-types.ts @@ -86,3 +86,13 @@ export interface Preferences { comment_rewards: '0%' | '50%' | '100%'; referral_system: 'enabled' | 'disabled'; } + +/** + * Metadata properties for SEO and page meta tags + */ +export interface MetadataProps { + tabTitle: string; + description: string; + image: string; + title: string; +} diff --git a/apps/blog/lib/get-metadata.ts b/packages/transaction/lib/metadata.ts similarity index 61% rename from apps/blog/lib/get-metadata.ts rename to packages/transaction/lib/metadata.ts index 8e4309d60c351d3606c0c298e298c7436b35bf63..cc528b134441ed23d3f27e097e46614fcf8d84d1 100644 --- a/apps/blog/lib/get-metadata.ts +++ b/packages/transaction/lib/metadata.ts @@ -1,16 +1,29 @@ -import { getAccountFull } from '@transaction/lib/hive-api'; -import { getCommunity } from '@transaction/lib/bridge-api'; +import { getAccountFull } from './hive-api'; +import { getCommunity } from './bridge-api'; +import { MetadataProps } from './app-types'; +// Re-export MetadataProps type for consumers +export type { MetadataProps } from './app-types'; + +const DEFAULT_IMAGE = 'https://hive.blog/images/hive-blog-share.png'; + +/** + * Get metadata for a user account page + * @param firstParam - The username with @ prefix (e.g. "@username") + * @param descriptionText - Text to prepend to description (e.g. "Posts by") + * @returns MetadataProps for SEO + */ export const getAccountMetadata = async ( firstParam: string, descriptionText: string ): Promise => { - let metadata = { + let metadata: MetadataProps = { tabTitle: '', description: '', image: '', title: firstParam }; + if (firstParam.startsWith('@')) { try { const username = firstParam.split('@')[1]; @@ -21,11 +34,10 @@ export const getAccountMetadata = async ( } const displayName = data.profile?.name || data.name; - const defaultImage = 'https://hive.blog/images/hive-blog-share.png'; metadata = { ...metadata, - image: data.profile?.profile_image || defaultImage, + image: data.profile?.profile_image || DEFAULT_IMAGE, tabTitle: displayName === username ? `${descriptionText} ${displayName} - Hive` @@ -37,14 +49,23 @@ export const getAccountMetadata = async ( console.error('Error fetching account:', error); } } + return metadata; }; + +/** + * Get metadata for a community page + * @param firstParam - Primary display param (e.g. page title) + * @param secondParam - Community identifier (e.g. "hive-123456") + * @param descriptionText - Text to prepend to description + * @returns MetadataProps for SEO + */ export const getCommunityMetadata = async ( firstParam: string, secondParam: string, descriptionText: string ): Promise => { - let metadata = { + let metadata: MetadataProps = { tabTitle: '', description: '', image: '', @@ -53,25 +74,25 @@ export const getCommunityMetadata = async ( try { if (secondParam === '' || !secondParam.startsWith('hive-')) { - const defaultMetadata = { + return { tabTitle: '', description: '', - image: 'https://hive.blog/images/hive-blog-share.png', + image: DEFAULT_IMAGE, title: firstParam }; - return defaultMetadata; } - // Fetch community data + const data = await getCommunity(secondParam); - // If the community data does not exist, throw an error - if (!data) throw new Error(`Community ${secondParam} not found`); - // If the community data exists, set the username to the community title or name + if (!data) { + throw new Error(`Community ${secondParam} not found`); + } + const communityName = data?.title ?? data.name; metadata.tabTitle = `${communityName} / ${firstParam} - Hive`; metadata.description = data?.description || `${descriptionText} ${secondParam}. Hive: Communities Without Borders.`; - metadata.image = data?.avatar_url || 'https://hive.blog/images/hive-blog-share.png'; + metadata.image = data?.avatar_url || DEFAULT_IMAGE; metadata.title = communityName; } catch (error) { console.error('Error fetching community:', error); @@ -79,9 +100,3 @@ export const getCommunityMetadata = async ( return metadata; }; -export interface MetadataProps { - tabTitle: string; - description: string; - image: string; - title: string; -} diff --git a/packages/ui/lib/logging.ts b/packages/ui/lib/logging.ts index 6e1c24de329792689e69d4f2b2410c93153a3924..63cd0edc65ebe2608dbfa64528862be71079aa4b 100644 --- a/packages/ui/lib/logging.ts +++ b/packages/ui/lib/logging.ts @@ -1,3 +1,15 @@ +/** + * Pino-based logging utilities for both server and client. + * This is the canonical logging implementation used across the monorepo. + * + * @example + * ```ts + * import { getLogger } from '@ui/lib/logging'; + * const logger = getLogger('app'); + * logger.info('message'); + * logger.error(error, 'error message'); // error first for Pino! + * ``` + */ import pino, { Logger } from 'pino'; import env from '@beam-australia/react-env'; diff --git a/apps/blog/utils/PathUtils.ts b/packages/ui/lib/path-utils.ts similarity index 92% rename from apps/blog/utils/PathUtils.ts rename to packages/ui/lib/path-utils.ts index e8655386a9a27677e000a8c68fb97dd4b81fa9dd..1d1bfd7f5df1ead4d89dd991d3885df42472d402 100644 --- a/apps/blog/utils/PathUtils.ts +++ b/packages/ui/lib/path-utils.ts @@ -3,28 +3,28 @@ import env from '@beam-australia/react-env'; /** * Prepends the configured base path to a given path * Gets basePath from runtime environment to match Next.js configuration - * @param path - The path to prepend the base path to + * @param path - The path to prepend the base path to * @returns The path with base path prepended */ export function withBasePath(path: string): string { // Get basePath from environment - const basePath = (typeof window !== 'undefined') + const basePath = (typeof window !== 'undefined') ? env('BASE_PATH') || '' : process.env.NEXT_PUBLIC_BASE_PATH || ''; - + // If no base path or path is already absolute URL, return as-is if (!basePath || path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) { return path; } - + // Handle hash and query parameters if (path.startsWith('#') || path.startsWith('?')) { return path; } - + // Ensure path starts with / const normalizedPath = path.startsWith('/') ? path : `/${path}`; - + // Combine base path with path, avoiding double slashes return `${basePath}${normalizedPath}`; } @@ -38,9 +38,9 @@ export function getBasePath(): string { if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASE_PATH) { return process.env.NEXT_PUBLIC_BASE_PATH; } - + // In browser, get from env() if available - const basePath = (typeof window !== 'undefined') + const basePath = (typeof window !== 'undefined') ? env('BASE_PATH') || '' : ''; return basePath; @@ -56,14 +56,14 @@ export function getImageSrc(src: string): string { if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) { return src; } - + const basePath = getBasePath(); - + // If no base path or src already includes it, return as-is if (!basePath || src.startsWith(basePath)) { return src; } - + // Prepend base path return `${basePath}${src.startsWith('/') ? src : '/' + src}`; } @@ -76,11 +76,11 @@ export function getImageSrc(src: string): string { */ export function removeBasePath(path: string): string { const basePath = getBasePath(); - + if (!basePath || !path.startsWith(basePath)) { return path; } - + const pathWithoutBase = path.slice(basePath.length); return pathWithoutBase.startsWith('/') ? pathWithoutBase : '/' + pathWithoutBase; -} \ No newline at end of file +} diff --git a/packages/ui/lib/utils.ts b/packages/ui/lib/utils.ts index deaabf4bb9c28459b7f53f54ecb450d87691790e..88d04ecf6230218d5cc4c742e5836098f7517039 100644 --- a/packages/ui/lib/utils.ts +++ b/packages/ui/lib/utils.ts @@ -141,12 +141,19 @@ export function isJSON(value: string) { } } -export const getCookie = (name: string) => { - if (typeof document === 'undefined') return null; +/** + * Return cookie value for given cookie name. For use on client only. + * When cookie doesn't exist returns empty string. + * + * @param name - Cookie name + * @returns Cookie value or empty string if not found + */ +export const getCookie = (name: string): string => { + if (typeof document === 'undefined') return ''; const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop()?.split(';').shift(); - return null; + if (parts.length === 2) return parts.pop()?.split(';').shift() ?? ''; + return ''; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37db92672cf423b6215c36a8cc90eeb1bb8b8e6f..8969140db00c131de9493468a4ff3e5ff28a91d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -415,6 +415,9 @@ importers: '@hive/eslint-config-custom': specifier: workspace:* version: link:../eslint-config-custom + '@hive/ui': + specifier: workspace:* + version: link:../ui pino: specifier: ^8.17.1 version: 8.21.0