feat: onboarding trial flow, Google sign-in, dark mode hotspot fixes
- Move trial creation from auth hook to dedicated /subscriptions/trial endpoint with eligibility checks (3-day Full Paket trial) - Add animated onboarding progress (Remotion) with confetti on completion - Register redirects to subscription page with welcome flow - Add Google social login/signup support with config injection - Improve hotspot overlay visibility in dark mode - Show "Panele Git" on landing page when authenticated - Turkish translations for API error messages - Add toast utility wrapper, UUID generation for auth IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
238
apps/web/src/remotion/OnboardingProgress.tsx
Normal file
238
apps/web/src/remotion/OnboardingProgress.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
} from "remotion";
|
||||
|
||||
function getColors(isDark: boolean) {
|
||||
return {
|
||||
bg: isDark ? "#1a1a1a" : "#f5f5f5",
|
||||
fg: isDark ? "#fafafa" : "#0a0a0a",
|
||||
muted: isDark ? "#262626" : "#e5e5e5",
|
||||
mutedFg: isDark ? "#a3a3a3" : "#737373",
|
||||
border: isDark ? "#333333" : "#d4d4d4",
|
||||
emerald: "#10b981",
|
||||
emeraldDark: "#059669",
|
||||
surface: isDark ? "#1f1f1f" : "#ffffff",
|
||||
trackBg: isDark ? "#262626" : "#e5e5e5",
|
||||
checkBg: isDark ? "#1f1f1f" : "#ffffff",
|
||||
};
|
||||
}
|
||||
|
||||
const STEPS = [
|
||||
{ label: "Hesap doğrulanıyor", endFrame: 60 },
|
||||
{ label: "Kataloglar hazırlanıyor", endFrame: 120 },
|
||||
{ label: "Full Paket aktif ediliyor", endFrame: 175 },
|
||||
{ label: "Tamamlandı!", endFrame: 210 },
|
||||
];
|
||||
|
||||
export const OnboardingProgress: React.FC<{
|
||||
isDark: boolean;
|
||||
stepLabels?: string[];
|
||||
}> = ({ isDark, stepLabels }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
const labels = stepLabels ?? STEPS.map((s) => s.label);
|
||||
|
||||
// Overall progress 0→1 over 210 frames with ease-in acceleration
|
||||
const rawProgress = interpolate(frame, [0, 210], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const progress = rawProgress * rawProgress * (3 - 2 * rawProgress); // smoothstep
|
||||
|
||||
const barWidth = 600;
|
||||
const barHeight = 8;
|
||||
const barX = 100;
|
||||
const barY = 100;
|
||||
|
||||
// Determine active step index
|
||||
const activeStepIndex = STEPS.findIndex((s) => frame < s.endFrame);
|
||||
const currentStep = activeStepIndex === -1 ? STEPS.length - 1 : activeStepIndex;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
{/* Active step label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: barY - 48,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{labels.map((label, i) => {
|
||||
const isActive = i === currentStep;
|
||||
const labelOpacity = isActive
|
||||
? spring({
|
||||
frame: frame - Math.max(0, i === 0 ? 0 : STEPS[i - 1].endFrame),
|
||||
fps,
|
||||
config: { damping: 20, stiffness: 120 },
|
||||
})
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
opacity: labelOpacity,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: i === STEPS.length - 1 && isActive ? c.emerald : c.fg,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Track background */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: barY,
|
||||
left: barX,
|
||||
width: barWidth,
|
||||
height: barHeight,
|
||||
borderRadius: barHeight / 2,
|
||||
backgroundColor: c.trackBg,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Track fill */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: barY,
|
||||
left: barX,
|
||||
width: barWidth * progress,
|
||||
height: barHeight,
|
||||
borderRadius: barHeight / 2,
|
||||
background: `linear-gradient(90deg, ${c.emerald}, ${c.emeraldDark})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Checkpoints */}
|
||||
{STEPS.map((step, i) => {
|
||||
const stepX = barX + (barWidth / (STEPS.length - 1)) * i;
|
||||
const stepY = barY + barHeight / 2;
|
||||
const reached = frame >= step.endFrame;
|
||||
const reaching = frame >= (i === 0 ? 0 : STEPS[i - 1].endFrame);
|
||||
|
||||
const fillProgress = reached
|
||||
? 1
|
||||
: reaching
|
||||
? spring({
|
||||
frame: frame - (i === 0 ? 0 : STEPS[i - 1].endFrame),
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 100 },
|
||||
durationInFrames: step.endFrame - (i === 0 ? 0 : STEPS[i - 1].endFrame),
|
||||
})
|
||||
: 0;
|
||||
|
||||
const checkScale = reached
|
||||
? spring({
|
||||
frame: frame - step.endFrame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 200 },
|
||||
})
|
||||
: 0;
|
||||
|
||||
const circleSize = 28;
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
{/* Outer circle */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: stepX - circleSize / 2,
|
||||
top: stepY - circleSize / 2,
|
||||
width: circleSize,
|
||||
height: circleSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c.checkBg,
|
||||
border: `2px solid ${fillProgress > 0 ? c.emerald : c.border}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Fill background */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: c.emerald,
|
||||
opacity: fillProgress,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
{/* Checkmark */}
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width={14}
|
||||
height={14}
|
||||
style={{
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
transform: `scale(${checkScale})`,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M5 13l4 4L19 7"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Step number below (visible when not yet reached) */}
|
||||
{!reached && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: stepX - circleSize / 2,
|
||||
top: stepY - circleSize / 2,
|
||||
width: circleSize,
|
||||
height: circleSize,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: fillProgress > 0.5 ? "white" : c.mutedFg,
|
||||
zIndex: 2,
|
||||
opacity: 1 - checkScale,
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user