-
Notifications
You must be signed in to change notification settings - Fork 221
fix(security): move newsletter subscription to server-side API route #465
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
base: main
Are you sure you want to change the base?
Changes from all commits
ae78b9c
194bdee
297d911
09995c9
a85c24d
4989fc3
be345de
6f13cac
6c39e4b
3d61559
f7d7636
fcc45db
6bc1330
ee3b561
edbe9a7
f49d4f9
6e1f115
277576a
5b135d2
ef6d39e
a7e535e
8e556e3
eebd6a3
d2d7db4
5f9b7d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| NEXT_PUBLIC_STORYBOOK_URL=http://localhost:6006 | ||
| NEXT_PUBLIC_STORYBOOK_URL=http://localhost:6007 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { HelpCircle, LogOut, Settings, User } from "lucide-react"; | ||
| import DropdownMenu, { type DropdownMenuProps } from "@/animata/navigation/dropdown-menu"; | ||
|
|
||
| const meta = { | ||
| title: "Navigation/Dropdown Menu", | ||
| component: DropdownMenu, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| tags: ["autodocs"], | ||
| argTypes: { | ||
| align: { | ||
| control: "select", | ||
| options: ["left", "right"], | ||
| description: "Dropdown alignment relative to trigger button", | ||
| }, | ||
| triggerLabel: { | ||
| control: "text", | ||
| description: "Label text for the trigger button", | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof DropdownMenu>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Primary: Story = { | ||
| args: { | ||
| triggerLabel: "Options", | ||
| align: "left", | ||
| items: [ | ||
| { label: "Profile", icon: <User className="h-4 w-4" /> }, | ||
| { label: "Settings", icon: <Settings className="h-4 w-4" /> }, | ||
| { label: "Help", icon: <HelpCircle className="h-4 w-4" /> }, | ||
| { label: "Sign Out", icon: <LogOut className="h-4 w-4" /> }, | ||
| ], | ||
| }, | ||
| render: (args) => ( | ||
| <div className="flex h-64 items-center justify-center"> | ||
| <DropdownMenu {...args} /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const RightAlign: Story = { | ||
| args: { | ||
| triggerLabel: "Menu", | ||
| align: "right", | ||
| items: [ | ||
| { label: "Profile", icon: <User className="h-4 w-4" /> }, | ||
| { label: "Settings", icon: <Settings className="h-4 w-4" /> }, | ||
| { label: "Help", icon: <HelpCircle className="h-4 w-4" /> }, | ||
| { label: "Sign Out", icon: <LogOut className="h-4 w-4" /> }, | ||
| ], | ||
| }, | ||
| render: (args) => ( | ||
| <div className="flex h-64 items-center justify-end pr-24"> | ||
| <DropdownMenu {...args} /> | ||
| </div> | ||
| ), | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,168 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { AnimatePresence, motion } from "motion/react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "@/lib/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface MenuItem { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon?: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface DropdownMenuProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| items?: MenuItem[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| triggerLabel?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| align?: "left" | "right"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultItems: MenuItem[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { label: "Profile" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { label: "Settings" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { label: "Help" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { label: "Sign Out" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function DropdownMenu({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| items = defaultItems, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| triggerLabel = "Options", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| align = "left", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: DropdownMenuProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isOpen, setIsOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [selectedIndex, setSelectedIndex] = useState(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const triggerRef = useRef<HTMLButtonElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const menuRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefersReducedMotion = useRef(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| prefersReducedMotion.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isOpen) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedIndex(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleKeyDown = (e: KeyboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === "ArrowDown") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedIndex((prev) => (prev + 1) % items.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (e.key === "ArrowUp") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedIndex((prev) => (prev - 1 + items.length) % items.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (e.key === "Enter") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard keyboard index math when On Line 50 and Line 53, modulo with Suggested patch const handleKeyDown = (e: KeyboardEvent) => {
+ if (items.length === 0) {
+ if (e.key === "Escape") {
+ e.preventDefault();
+ setIsOpen(false);
+ triggerRef.current?.focus();
+ }
+ return;
+ }
+
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % items.length);π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| items[selectedIndex]?.onClick?.(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (e.key === "Escape") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| triggerRef.current?.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener("keydown", handleKeyDown); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => window.removeEventListener("keydown", handleKeyDown); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [isOpen, selectedIndex, items]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleClickOutside = (e: MouseEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| menuRef.current && | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| triggerRef.current && | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| !menuRef.current.contains(e.target as Node) && | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| !triggerRef.current.contains(e.target as Node) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isOpen) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener("mousedown", handleClickOutside); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => document.removeEventListener("mousedown", handleClickOutside); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [isOpen]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const animationProps = prefersReducedMotion.current | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| initial: { opacity: 0, translateY: -8 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| animate: { opacity: 1, translateY: 0 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit: { opacity: 0, translateY: -8 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative inline-block"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={triggerRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setIsOpen(!isOpen)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-haspopup="menu" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-expanded={isOpen} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "min-h-11 min-w-11 inline-flex items-center justify-center gap-2 rounded-lg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "bg-background border border-border px-3 py-2 text-sm font-medium", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text-foreground transition-colors duration-200", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "hover:bg-muted hover:text-muted-foreground", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "dark:focus:ring-offset-background", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "active:scale-95", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {triggerLabel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn("h-4 w-4 transition-transform duration-200", isOpen && "rotate-180")} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| stroke="currentColor" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewBox="0 0 24 24" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| strokeLinecap="round" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| strokeLinejoin="round" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| strokeWidth={2} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| d="M19 14l-7 7m0 0l-7-7m7 7V3" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AnimatePresence> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {isOpen && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <motion.div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={menuRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="menu" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "absolute z-50 mt-2 min-w-48 overflow-hidden rounded-lg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "border border-border bg-background shadow-lg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "dark:border-border dark:bg-background", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| align === "right" ? "right-0" : "left-0", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {...animationProps} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {items.map((item, index) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={index} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="menuitem" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| item.onClick?.(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "w-full min-h-11 inline-flex items-center gap-3 px-4 py-3", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text-sm font-medium transition-colors duration-150", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text-foreground hover:bg-muted hover:text-muted-foreground", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "focus:outline-none focus:bg-muted focus:text-muted-foreground", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "dark:text-foreground dark:hover:bg-muted dark:focus:bg-muted", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedIndex === index && "bg-muted text-muted-foreground", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseEnter={() => setSelectedIndex(index)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {item.icon && <span className="shrink-0">{item.icon}</span>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{item.label}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </motion.div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </AnimatePresence> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
Repository: codse/animata
Length of output: 378
π Web query:
@storybook/nextjs nextConfigPath relative path resolution base directoryπ‘ Result:
@storybook/nextjs: nextConfigPath must be given as an absolute path; Storybook will not do relative-path resolution for you. The documented usage is to compute it from your projectβs base directory (commonly the current working directory) before passing it. In Storybookβs Next.js framework docs, nextConfigPath is described as βThe absolute path to the next.config.js fileβ and is explicitly positioned as an option for cases where your next.config.js is not in the project root [1]. The examples show resolving it with Nodeβs path.resolve(process.cwd, 'next.config.js') from your Storybook main config, meaning the effective βbase directoryβ is whatever directory Storybook is run from / what process.cwd is at config evaluation time [1]. So for your question (relative path resolution + base directory): - Use an absolute path. Donβt pass a relative string. - If you want to build the absolute path from a relative location, anchor that relative location to process.cwd (or otherwise to the directory you consider the project root) just like the docs example: path.resolve(process.cwd, 'next.config.js') [1].Citations:
π Script executed:
Repository: codse/animata
Length of output: 921
Fix
nextConfigPathin.storybook/main.tsto point to the repo-rootnext.config.mjs(absolute path).Current code uses
nextConfigPath: "./next.config.mjs", which resolves to.storybook/next.config.mjs(file doesnβt exist), and@storybook/nextjsrequiresnextConfigPathto be an absolute path. Update to something likepath.resolve(process.cwd, "next.config.mjs")(addimport path from "path").π€ Prompt for AI Agents