Skip to content
Merged
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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# App Preview Converter
# 🍎 Ciderpress

Convert your videos to the required format for macOS and iOS App Store app previews.
Press your app preview videos into App Store perfection.

**Why this exists:** When uploading MP4 app recordings to Apple's App Store Connect, uploads often fail silently without any indication why. Apple secretly checks for specific video formatting requirements. This tool applies those formats to your video so it can be properly uploaded.
**Why this exists:** When uploading MP4 app recordings to Apple's App Store Connect, uploads often fail silently without any indication why. Apple secretly checks for specific video formatting requirements. Ciderpress applies those formats to your video so it can be properly uploaded.

## Features

Expand Down Expand Up @@ -73,8 +73,8 @@ bun test
# Run tests with coverage
bun test:coverage

# Lint
bun lint
# Lint & format check
bun run check

# Build for production
bun run build
Expand All @@ -87,6 +87,7 @@ bun run build
- **Styling:** Tailwind CSS
- **UI Components:** shadcn/ui + Magic UI
- **Animation:** Motion (Framer Motion)
- **Linting:** Biome
- **Testing:** Vitest + React Testing Library
- **Video Processing:** FFmpeg (server-side)

Expand All @@ -98,9 +99,8 @@ src/
│ ├── api/convert/ # Video conversion API endpoint
│ └── page.tsx # Main page
├── components/
│ ├── magicui/ # Magic UI components
│ ├── providers/ # React context providers
│ ├── ui/ # shadcn/ui components
│ ├── ui/ # UI components (shadcn + custom)
│ └── video-convert/ # Video conversion flow
├── hooks/ # Custom React hooks
├── lib/ # Utility functions
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/useTerminalMessages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("useTerminalMessages", () => {
result.current.initializeMessages();
});

expect(result.current.messages).toHaveLength(2);
expect(result.current.messages).toHaveLength(3);
expect(result.current.messages[0].text).toContain("Welcome");
expect(result.current.messages[0].type).toBe("info");
});
Expand All @@ -30,8 +30,8 @@ describe("useTerminalMessages", () => {
result.current.addUploadPrompt(mockCallback);
});

expect(result.current.messages).toHaveLength(3);
const uploadPrompt = result.current.messages[2];
expect(result.current.messages).toHaveLength(4);
const uploadPrompt = result.current.messages[3];
expect(uploadPrompt.type).toBe("prompt");
expect(uploadPrompt.buttons).toBeDefined();
expect(uploadPrompt.buttons?.[0].action).toBe("upload");
Expand All @@ -45,7 +45,7 @@ describe("useTerminalMessages", () => {
result.current.addPlatformPrompt();
});

const platformPrompt = result.current.messages[2];
const platformPrompt = result.current.messages[3];
expect(platformPrompt.type).toBe("prompt");
expect(platformPrompt.buttons).toHaveLength(2);
expect(platformPrompt.buttons?.[0].action).toBe("macos");
Expand Down Expand Up @@ -90,15 +90,15 @@ describe("useTerminalMessages", () => {
expect(audioPrompt.buttons?.[1].action).toBe("audio-no");
});

it("should add error message with warning emoji", () => {
it("should add error message with bruised apple indicator", () => {
const { result } = renderHook(() => useTerminalMessages());

act(() => {
result.current.addErrorMessage("Something went wrong");
});

expect(result.current.messages[0].type).toBe("error");
expect(result.current.messages[0].text).toContain("⚠️");
expect(result.current.messages[0].text).toContain("Bruised apple");
expect(result.current.messages[0].text).toContain("Something went wrong");
});

Expand Down Expand Up @@ -129,6 +129,6 @@ describe("useTerminalMessages", () => {

// Support message replaces all messages
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0].text).toContain("done");
expect(result.current.messages[0].text).toContain("pressed");
});
});
23 changes: 16 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,30 @@ const jetbrainsMono = JetBrains_Mono({
});

export const metadata: Metadata = {
title: "App Preview Converter",
title: "Ciderpress - App Preview Converter",
description:
"Convert your app preview videos for macOS or iOS App Store submissions. Fixes common Apple upload rejections with proper formatting.",
keywords: ["app preview", "app store", "video converter", "macOS", "iOS", "ffmpeg", "apple"],
"Press your app preview videos into App Store perfection. Fixes common Apple upload rejections with proper formatting for macOS and iOS.",
keywords: [
"app preview",
"app store",
"video converter",
"macOS",
"iOS",
"ffmpeg",
"apple",
"ciderpress",
],
authors: [{ name: "Brenden Bishop" }],
openGraph: {
title: "App Preview Converter",
description: "Convert your app preview videos for macOS or iOS App Store submissions",
title: "Ciderpress",
description: "Press your app preview videos into App Store perfection",
type: "website",
locale: "en_US",
},
twitter: {
card: "summary",
title: "App Preview Converter",
description: "Convert your app preview videos for macOS or iOS App Store submissions",
title: "Ciderpress",
description: "Press your app preview videos into App Store perfection",
},
robots: {
index: true,
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export default function TerminalContent({
};

return (
<Terminal title="App Preview Converter">
<Terminal>
{isMounted && <>{messages.map((message, index) => renderMessage(message, index))}</>}
</Terminal>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/animated-upload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const AnimatedUploadButton = React.forwardRef<HTMLButtonElement, Animated
>
<span className="inline-flex items-center">
<Check className="mr-2 size-4" />
Uploaded
Picked!
</span>
</motion.span>
</motion.button>
Expand All @@ -68,7 +68,7 @@ export const AnimatedUploadButton = React.forwardRef<HTMLButtonElement, Animated
>
<span className="inline-flex items-center">
<Loader2 className="mr-2 size-4 animate-spin" />
Uploading...
Picking...
</span>
</motion.span>
</motion.button>
Expand All @@ -92,7 +92,7 @@ export const AnimatedUploadButton = React.forwardRef<HTMLButtonElement, Animated
exit={{ x: 50, transition: { duration: 0.1 } }}
>
<span className="group inline-flex items-center">
Upload
Pick Apple
<Upload className="ml-2 size-4 transition-transform duration-300 group-hover:translate-y-[-2px]" />
</span>
</motion.span>
Expand Down
66 changes: 66 additions & 0 deletions src/components/ui/ciderpress-logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Apple } from "lucide-react";
import { cn } from "@/lib/utils";

interface CiderpressLogoProps {
size?: "sm" | "md" | "lg" | "xl";
className?: string;
showText?: boolean;
}

const sizeMap = {
sm: { icon: 20, press: 16, gap: 1, text: "text-sm" },
md: { icon: 28, press: 22, gap: 1.5, text: "text-lg" },
lg: { icon: 40, press: 32, gap: 2, text: "text-2xl" },
xl: { icon: 56, press: 44, gap: 3, text: "text-3xl" },
};

function PressLines({ width, height, gap }: { width: number; height: number; gap: number }) {
const lineStyle = { width: `${width}px`, height: `${height}px` };
return (
<div className="flex flex-col justify-center" style={{ gap: `${gap}px` }}>
<div className="bg-amber-500 rounded-full" style={lineStyle} />
<div className="bg-amber-500 rounded-full" style={lineStyle} />
<div className="bg-amber-500 rounded-full" style={lineStyle} />
</div>
);
}

export function CiderpressLogo({ size = "md", className, showText = false }: CiderpressLogoProps) {
const { icon, press, gap, text } = sizeMap[size];
const lineWidth = press * 0.4;
const lineHeight = press * 0.08;

return (
<div className={cn("flex items-center", showText && "gap-3", className)}>
{/* Logo Mark */}
<div className="relative flex items-center" style={{ gap: `${gap * 4}px` }}>
{/* Left Press Lines */}
<PressLines width={lineWidth} height={lineHeight} gap={gap} />

{/* Apple Icon */}
<div className="relative">
<Apple size={icon} className="text-red-500 fill-red-500/20" strokeWidth={1.5} />
{/* Juice Drop */}
<div
className="absolute left-1/2 -translate-x-1/2 bg-amber-400 rounded-full animate-pulse"
style={{
width: `${icon * 0.15}px`,
height: `${icon * 0.2}px`,
bottom: `${-icon * 0.15}px`,
}}
/>
</div>

{/* Right Press Lines */}
<PressLines width={lineWidth} height={lineHeight} gap={gap} />
</div>

{/* Wordmark */}
{showText && (
<span className={cn("font-mono font-bold tracking-tight text-white", text)}>
Ciderpress
</span>
)}
</div>
);
}
7 changes: 6 additions & 1 deletion src/components/ui/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { type MotionProps, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { BorderBeam } from "@/components/ui/border-beam";
import { CiderpressLogo } from "@/components/ui/ciderpress-logo";
import { cn } from "@/lib/utils";

// Animation timing constants
Expand Down Expand Up @@ -237,7 +238,11 @@ export const Terminal = ({ children, className, title }: TerminalProps) => {
<div className="h-3 w-3 rounded-full bg-yellow-500/80 hover:bg-yellow-500 transition-colors"></div>
<div className="h-3 w-3 rounded-full bg-green-500/80 hover:bg-green-500 transition-colors"></div>
</div>
<div className="text-sm font-mono text-neutral-300 tracking-tight">{title}</div>
{title ? (
<div className="text-sm font-mono text-neutral-300 tracking-tight">{title}</div>
) : (
<CiderpressLogo size="sm" showText />
)}
</div>
</div>

Expand Down
33 changes: 19 additions & 14 deletions src/hooks/useTerminalMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@ export const useTerminalMessages = () => {
const initializeMessages = useCallback(() => {
const initialMessages: TerminalMessage[] = [
{
text: "Welcome to App Preview Converter v1.0.0",
text: "🍎 Welcome to Ciderpress v1.0.0",
delay: TIMING.INSTANT, // First message starts immediately
type: "info",
},
{
text: "This tool helps you convert videos for App Store submissions",
text: "App preview video rejected? Apple keeps the reasons to themselves.",
delay: TIMING.BEAT, // Brief pause after welcome
type: "info",
},
{
text: "We'll press your video into the format they actually accept.",
delay: TIMING.BEAT,
type: "info",
},
];
setMessages(initialMessages);
}, []);
Expand All @@ -33,12 +38,12 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: "Upload a video to begin:",
text: "Drop an apple in the press to begin:",
type: "prompt",
delay: TIMING.PAUSE, // Pause before prompt
buttons: [
{
text: "Upload",
text: "Pick Apple",
action: "upload",
onAction: (file?: File) => {
if (file) {
Expand All @@ -55,7 +60,7 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: ` ${fileName} uploaded successfully!`,
text: `🍏 ${fileName} picked successfully!`,
delay: TIMING.INSTANT, // Immediate response
type: "success",
},
Expand All @@ -66,7 +71,7 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: "Which platform are you targeting?",
text: "Which orchard are you targeting?",
delay: TIMING.BEAT, // Small pause after success
type: "prompt",
buttons: [
Expand All @@ -82,7 +87,7 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: `✓ Platform set to ${platform} (${resolution})`,
text: `✓ Orchard set to ${platform} (${resolution})`,
delay: TIMING.INSTANT,
type: "success",
},
Expand Down Expand Up @@ -131,7 +136,7 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: "Starting video conversion...",
text: "🍏 Pressing...",
delay: TIMING.BEAT,
type: "info",
},
Expand All @@ -142,12 +147,12 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: "✓ Video is now ready for App Preview upload!",
text: "🧃 Fresh cider ready! Your video is App Store approved.",
delay: TIMING.PAUSE, // Pause for effect
type: "success",
buttons: [
{ text: "Download", action: "download", type: "rainbow" },
{ text: "New Conversion", action: "restart" },
{ text: "Bottle It", action: "download", type: "rainbow" },
{ text: "Press Another", action: "restart" },
],
},
]);
Expand All @@ -156,12 +161,12 @@ export const useTerminalMessages = () => {
const addSupportMessage = useCallback(() => {
setMessages([
{
text: "All done. Thank you & enjoy!",
text: "🍎 All pressed. Thank you & enjoy your cider!",
delay: TIMING.INSTANT,
type: "info",
buttons: [
{ text: "Buy me a coffee", action: "bmc", type: "bmc" },
{ text: "New Conversion", action: "restart" },
{ text: "Press Another", action: "restart" },
],
},
]);
Expand All @@ -171,7 +176,7 @@ export const useTerminalMessages = () => {
setMessages((prev) => [
...prev,
{
text: `⚠️ ${message}`,
text: `🍂 Bruised apple: ${message}`,
delay: TIMING.INSTANT,
type: "error",
},
Expand Down