Skip to content

Commit fa04d01

Browse files
committed
feat: extracted switch from subscriptionConfigurator and used in
actionTriggerView
1 parent 2633bf5 commit fa04d01

3 files changed

Lines changed: 89 additions & 79 deletions

File tree

src/components/ActionTriggerView.tsx

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"use client"
22

33
import { type ReactNode, useState } from "react"
4-
import { SegmentedControl, SegmentedControlItem, Text } from "@code0-tech/pictor"
54
import { ActionTriggerCard } from "@/components/ActionTriggerCard"
5+
import { Switch, type SwitchOption } from "@/components/ui/Switch"
66
import type { ExtractedActionTriggerItem, ExtractedFunctionDef, ExtractedTrigger } from "@/lib/actionTriggerExtraction"
77

88
interface ActionTriggerViewProps {
@@ -11,7 +11,7 @@ interface ActionTriggerViewProps {
1111
functionDefs: Array<{ item: ExtractedFunctionDef; icon: ReactNode }>
1212
}
1313

14-
const itemClassName = "text-secondary! transition-colors! data-[state=on]:bg-brand/20! data-[state=on]:text-brand!"
14+
type ViewMode = "both" | "triggers" | "functionDefs"
1515

1616
interface DisplayItem {
1717
type: "trigger" | "functionDef"
@@ -20,7 +20,12 @@ interface DisplayItem {
2020
}
2121

2222
export function ActionTriggerView({ locale, triggers, functionDefs }: ActionTriggerViewProps) {
23-
const [viewMode, setViewMode] = useState<"both" | "triggers" | "functionDefs">("both")
23+
const [viewMode, setViewMode] = useState<ViewMode>("both")
24+
const viewModeOptions: SwitchOption<ViewMode>[] = [
25+
{ value: "both", label: locale === "en" ? "Both" : "Beide" },
26+
{ value: "triggers", label: "Triggers" },
27+
{ value: "functionDefs", label: "FunctionDefinitions" },
28+
]
2429

2530
const visibleItems: DisplayItem[] = [
2631
...(viewMode === "both" || viewMode === "triggers" ? triggers.map(({ item, icon }) => ({ type: "trigger" as const, item, icon })) : []),
@@ -29,26 +34,7 @@ export function ActionTriggerView({ locale, triggers, functionDefs }: ActionTrig
2934

3035
return (
3136
<div className="space-y-6">
32-
<SegmentedControl
33-
type="single"
34-
value={viewMode}
35-
onValueChange={(value: string) => {
36-
if (value === "both" || value === "triggers" || value === "functionDefs") {
37-
setViewMode(value)
38-
}
39-
}}
40-
className={"h-10! w-max!"}
41-
>
42-
<SegmentedControlItem value="both" className={itemClassName}>
43-
<Text>{locale === "en" ? "Both" : "Beide"}</Text>
44-
</SegmentedControlItem>
45-
<SegmentedControlItem value="triggers" className={itemClassName}>
46-
<Text>Triggers</Text>
47-
</SegmentedControlItem>
48-
<SegmentedControlItem value="functionDefs" className={itemClassName}>
49-
<Text>FunctionDefinitions</Text>
50-
</SegmentedControlItem>
51-
</SegmentedControl>
37+
<Switch value={viewMode} options={viewModeOptions} onChange={setViewMode} className="w-max" />
5238

5339
<div className="flex flex-col gap-4 md:hidden">
5440
{visibleItems.map(({ type, item, icon }) => (

src/components/SubscriptionConfigurator.tsx

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"
88
import { BuyMenu } from "@/components/BuyMenu"
99
import { WorkflowCalculatorDialog } from "@/components/dialogs/WorkflowCalculatorDialog"
1010
import { Slider } from "@/components/ui/Slider"
11+
import { Switch, type SwitchOption } from "@/components/ui/Switch"
1112
import { IconCheck } from "@tabler/icons-react"
1213
import type { CSSProperties, ReactNode } from "react"
1314
import { useState } from "react"
@@ -91,17 +92,10 @@ interface AdditionalFeatureCardProps {
9192
formattedPrice: string
9293
}
9394

94-
interface PaymentPeriodSwitchProps {
95-
content: SubscriptionConfigData["paymentPeriod"]
96-
locale: AppLocale
97-
value: PaymentPeriod
98-
onChange: (value: PaymentPeriod) => void
99-
}
100-
10195
const paymentPeriodOptions = [
102-
{ value: "monthly", textKey: "monthlyText", indicatorClassName: "translate-x-0" },
103-
{ value: "quarterly", textKey: "quarterlyText", indicatorClassName: "translate-x-full" },
104-
{ value: "yearly", textKey: "yearlyText", indicatorClassName: "translate-x-[200%]" },
96+
{ value: "monthly", textKey: "monthlyText" },
97+
{ value: "quarterly", textKey: "quarterlyText" },
98+
{ value: "yearly", textKey: "yearlyText" },
10599
] as const
106100

107101
function OptionCard({ title, description, active, onClick, icon, disabled = false, accent = "aqua" }: OptionCardProps) {
@@ -138,49 +132,6 @@ function OptionCard({ title, description, active, onClick, icon, disabled = fals
138132
)
139133
}
140134

141-
function PaymentPeriodSwitch({ content, locale, value, onChange }: PaymentPeriodSwitchProps) {
142-
const activeOption = paymentPeriodOptions.find((option) => option.value === value) ?? paymentPeriodOptions[0]
143-
144-
return (
145-
<div className="space-y-3">
146-
<div>
147-
<p className="text-base text-secondary">{content.label}</p>
148-
<p className="mt-1 text-sm text-tertiary">{content.description}</p>
149-
</div>
150-
<div className="relative grid grid-cols-3 overflow-hidden rounded-2xl border border-white/10 bg-white/3 p-1">
151-
<div
152-
className={cn(
153-
"absolute left-1 top-1 h-[calc(100%-0.5rem)] w-[calc((100%-0.5rem)/3)] rounded-xl bg-white/10 transition-transform duration-300 ease-out",
154-
activeOption.indicatorClassName
155-
)}
156-
/>
157-
{paymentPeriodOptions.map((option) => {
158-
const active = value === option.value
159-
const discount = getPaymentPeriodDiscount(option.value, content)
160-
161-
return (
162-
<button
163-
key={option.value}
164-
type="button"
165-
onClick={() => onChange(option.value)}
166-
className={cn("relative z-10 min-w-0 rounded-xl px-3 py-2 text-sm font-medium transition-colors", active ? "text-white" : "text-secondary hover:text-white")}
167-
>
168-
<span className="inline-flex min-w-0 items-center justify-center gap-1">
169-
<span className="min-w-0 truncate">{content[option.textKey]}</span>
170-
{discount > 0 ? (
171-
<span className={cn("shrink-0 rounded-full -mt-3 px-1 py-0.5 text-[10px] tracking-wider leading-none bg-brand/15 text-brand")}>
172-
-{formatDiscountBadge(discount, locale)}
173-
</span>
174-
) : null}
175-
</span>
176-
</button>
177-
)
178-
})}
179-
</div>
180-
</div>
181-
)
182-
}
183-
184135
function FeatureRow({ icon, title, description }: FeatureRowProps) {
185136
return (
186137
<div className="flex flex-col gap-2">
@@ -282,6 +233,15 @@ export function SubscriptionConfigurator({ locale, content, icons }: { locale: A
282233
const paymentPeriodSuffix = getPaymentPeriodSuffix(selection.paymentPeriod, content.paymentPeriod)
283234
const totalBeforeDiscount = workflowExecutionPrice + aiTokenPrice + additionalFeaturesPrice
284235
const totalPrice = totalBeforeDiscount * (1 - paymentPeriodDiscount)
236+
const paymentPeriodSwitchOptions: SwitchOption<PaymentPeriod>[] = paymentPeriodOptions.map((option) => {
237+
const discount = getPaymentPeriodDiscount(option.value, content.paymentPeriod)
238+
239+
return {
240+
value: option.value,
241+
label: content.paymentPeriod[option.textKey],
242+
badge: discount > 0 ? `-${formatDiscountBadge(discount, locale)}` : null,
243+
}
244+
})
285245

286246
const subscribeHref = (() => {
287247
const searchParams = new URLSearchParams({
@@ -324,9 +284,10 @@ export function SubscriptionConfigurator({ locale, content, icons }: { locale: A
324284
<div className="relative z-10 flex flex-col gap-8">
325285
<h2 className="text-2xl font-semibold text-white lg:text-3xl">{content.optionsPanelHeading}</h2>
326286

327-
<PaymentPeriodSwitch
328-
content={content.paymentPeriod}
329-
locale={locale}
287+
<Switch
288+
label={content.paymentPeriod.label}
289+
description={content.paymentPeriod.description}
290+
options={paymentPeriodSwitchOptions}
330291
value={selection.paymentPeriod}
331292
onChange={(paymentPeriod) => setSelection((current) => ({ ...current, paymentPeriod }))}
332293
/>

src/components/ui/Switch.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"use client"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
export interface SwitchOption<TValue extends string> {
6+
value: TValue
7+
label: string
8+
badge?: string | null
9+
}
10+
11+
interface SwitchProps<TValue extends string> {
12+
label?: string
13+
description?: string
14+
value: TValue
15+
options: readonly SwitchOption<TValue>[]
16+
onChange: (value: TValue) => void
17+
className?: string
18+
}
19+
20+
export function Switch<TValue extends string>({ label, description, value, options, onChange, className }: SwitchProps<TValue>) {
21+
const activeIndex = Math.max(
22+
options.findIndex((option) => option.value === value),
23+
0
24+
)
25+
const columnCount = Math.max(options.length, 1)
26+
27+
return (
28+
<div className={cn("space-y-3", className)}>
29+
{label || description ? (
30+
<div>
31+
{label ? <p className="text-base text-secondary">{label}</p> : null}
32+
{description ? <p className="mt-1 text-sm text-tertiary">{description}</p> : null}
33+
</div>
34+
) : null}
35+
<div className="relative grid overflow-hidden rounded-2xl border border-white/10 bg-white/3 p-1" style={{ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))` }}>
36+
<div
37+
className="absolute left-1 top-1 h-[calc(100%-0.5rem)] rounded-xl bg-white/10 transition-transform duration-300 ease-out"
38+
style={{
39+
width: `calc((100% - 0.5rem) / ${columnCount})`,
40+
transform: `translateX(${activeIndex * 100}%)`,
41+
}}
42+
/>
43+
{options.map((option) => {
44+
const active = value === option.value
45+
46+
return (
47+
<button
48+
key={option.value}
49+
type="button"
50+
onClick={() => onChange(option.value)}
51+
className={cn("relative z-10 min-w-0 rounded-xl px-3 py-2 text-sm font-medium transition-colors", active ? "text-white" : "text-secondary hover:text-white")}
52+
>
53+
<span className="inline-flex min-w-0 items-center justify-center gap-1">
54+
<span className="min-w-0 truncate">{option.label}</span>
55+
{option.badge && <span className="shrink-0 rounded-full -mt-3 bg-brand/15 px-1 py-0.5 text-[10px] leading-none tracking-wider text-brand">{option.badge}</span>}
56+
</span>
57+
</button>
58+
)
59+
})}
60+
</div>
61+
</div>
62+
)
63+
}

0 commit comments

Comments
 (0)