Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/addon-catalog/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Nunito_Sans as nunitoSans } from 'next/font/google';
import { Header, Footer, Container } from '@repo/ui';
import { GoogleAnalytics } from '@next/third-parties/google';
import { Providers } from './providers';
import { ScrollToTop } from '../components/scroll-to-top';

import '@docsearch/css';
import './globals.css';
Expand Down Expand Up @@ -50,6 +51,7 @@ export default async function RootLayout({
<Container>{children}</Container>
</Providers>
<Footer />
<ScrollToTop />
</body>
<GoogleAnalytics gaId="G-MN8NJ34M7T" />
</html>
Expand Down
25 changes: 23 additions & 2 deletions apps/addon-catalog/components/home-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { type ReactNode, useEffect, useState } from 'react';
import { type ReactNode, useEffect, useState, useRef } from 'react';
import {
BookIcon,
CloseIcon,
Expand Down Expand Up @@ -41,6 +41,20 @@ export const HomeWrapper = ({ tagLinks, children }: HomeProps) => {
recipes: Recipe[];
}>({ addons: [], recipes: [] });
const pathname = usePathname();
const searchInputRef = useRef<HTMLInputElement>(null);

// Keyboard shortcut: Ctrl+K or Cmd+K to focus search
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
searchInputRef.current?.focus();
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

useEffect(() => {
setLoading(true);
Expand Down Expand Up @@ -82,13 +96,20 @@ export const HomeWrapper = ({ tagLinks, children }: HomeProps) => {
<div className="relative flex h-10 w-full flex-shrink-0 items-center rounded-full border border-zinc-300 md:w-[250px] dark:border-slate-700">
<SearchIcon className="absolute left-4 dark:text-slate-500" />
<input
className="h-full w-full rounded-full bg-transparent pl-10 placeholder:text-slate-500 dark:placeholder:text-slate-400"
ref={searchInputRef}
className="h-full w-full rounded-full bg-transparent pl-10 pr-20 placeholder:text-slate-500 dark:placeholder:text-slate-400"
placeholder="Search integrations"
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
title="Press Ctrl+K or ⌘K to focus"
/>
{!search && (
<kbd className="absolute right-3 hidden rounded border border-zinc-300 bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 md:inline-block dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400">
⌘K
</kbd>
)}
{search.length > 0 && (
<div
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 cursor-pointer items-center justify-center"
Expand Down
20 changes: 18 additions & 2 deletions apps/addon-catalog/components/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,27 @@ interface PreviewProps {

export const Preview = ({ element, orientation, type }: PreviewProps) => {
const isRecipe = type === 'recipe';
const Comp = isRecipe ? 'a' : Link;
// Determine if the link should be local or external
let href = '';
let isExternal = false;
if (isRecipe) {
href = `/recipes/${element.name ?? ''}`;
} else {
// For addons, check if the name starts with '@' (scoped package) or contains a slash
// If so, assume it's not handled locally and link to the external Storybook site
if (element.name && (element.name.startsWith('@') || element.name.includes('/'))) {
href = `https://storybook.js.org/addons/${element.name}`;
isExternal = true;
} else {
href = `/${element.name ?? ''}`;
}
}
const Comp = isExternal ? 'a' : Link;

return (
<Comp
href={`${isRecipe ? '/recipes' : ''}/${element.name ?? ''}`}
href={href}
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className={cn(
'flex justify-between rounded border border-zinc-300 p-6 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:border-blue-500 dark:border-slate-800 dark:hover:border-blue-500',
orientation === 'horizontal'
Expand Down
48 changes: 48 additions & 0 deletions apps/addon-catalog/components/scroll-to-top.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import { useEffect, useState } from 'react';
import { ArrowUpIcon } from '@storybook/icons';
import { cn } from '@repo/utils';

export function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
const toggleVisibility = () => {
// Show button when page is scrolled down 300px
if (window.scrollY > 300) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};

window.addEventListener('scroll', toggleVisibility);

return () => {
window.removeEventListener('scroll', toggleVisibility);
};
}, []);

const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};

return (
<button
onClick={scrollToTop}
className={cn(
'fixed bottom-8 right-8 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-blue-500 text-white shadow-lg transition-all duration-300 hover:bg-blue-600 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:bg-blue-600 dark:hover:bg-blue-700',
isVisible
? 'translate-y-0 opacity-100'
: 'pointer-events-none translate-y-16 opacity-0'
)}
aria-label="Scroll to top"
>
<ArrowUpIcon className="h-6 w-6" />
</button>
);
}
2 changes: 1 addition & 1 deletion apps/addon-catalog/lib/fetch-search-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function fetchSearchData(query: string) {
try {
const data = await fetchAddonsQuery<SearchData, { query: string }>(
gql`
query Search($query: String!) {
query Search($query: string!) {
partialSearchIntegrations(query: $query) {
addons {
${addonFragment}
Expand Down
2 changes: 2 additions & 0 deletions apps/frontpage/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { cn } from '@repo/utils';
import { GoogleAnalytics } from '@next/third-parties/google';
import { Providers } from './providers';
import { ScrollToTop } from '../components/scroll-to-top';

import '@docsearch/css';
import './globals.css';
Expand Down Expand Up @@ -60,6 +61,7 @@ export default function RootLayout({
)}
>
<Providers>{children}</Providers>
<ScrollToTop />
</body>
<GoogleAnalytics gaId="G-MN8NJ34M7T" />
</html>
Expand Down
12 changes: 12 additions & 0 deletions apps/frontpage/app/showcase/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import dynamic from 'next/dynamic';

// Dynamically import HomeWrapper from addon-catalog for SSR compatibility
const HomeWrapper = dynamic(
() => import('../../../addon-catalog/components/home-wrapper').then(mod => mod.HomeWrapper),
{ ssr: false }
);

export default function ShowcasePage() {
// The HomeWrapper will handle search and display logic
return <HomeWrapper />;
}
14 changes: 12 additions & 2 deletions apps/frontpage/components/docs/footer/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,15 @@ export async function sendFeedback(
const headersList = headers();

try {
// Skip strict validation in development mode
if (!siteUrl) {
console.info('Development mode: Simulating successful feedback submission');
return {
status: 'ok',
message: 'Feedback received (development mode)',
};
}

const ip =
headersList.get('x-real-ip') ??
headersList.get('x-forwarded-for') ??
Expand All @@ -508,9 +517,10 @@ export async function sendFeedback(

const path = slug.replace('/docs', '');

const origin = headersList.get('origin');
const hasValidOrigin = siteUrl
? headersList.get('origin') === process.env.URL
: true;
? origin === 'https://storybook.js.org'
: origin?.startsWith('http://localhost:') ?? true;
const hasValidReferer = headersList.get('referer')?.endsWith(path);

if (!hasValidOrigin || !hasValidReferer || spuriousComment) {
Expand Down
45 changes: 45 additions & 0 deletions apps/frontpage/components/scroll-to-top.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client';

import { useEffect, useState } from 'react';
import { ArrowUpIcon } from '@storybook/icons';
import { cn } from '@repo/utils';

export function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
const toggleVisibility = () => {
if (window.scrollY > 300) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};

window.addEventListener('scroll', toggleVisibility);

return () => window.removeEventListener('scroll', toggleVisibility);
}, []);

const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};

return (
<button
onClick={scrollToTop}
className={cn(
'fixed bottom-8 right-8 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-blue-500 text-white shadow-lg transition-all duration-300 hover:bg-blue-600 hover:shadow-xl',
isVisible
? 'translate-y-0 opacity-100'
: 'translate-y-16 opacity-0 pointer-events-none',
)}
aria-label="Scroll to top"
>
<ArrowUpIcon className="h-6 w-6" />
</button>
);
}