This commit is contained in:
HugeFrog24
2026-04-25 14:00:45 +02:00
parent 4f2d4c4a59
commit 33af88d79d
68 changed files with 8587 additions and 6868 deletions
+41
View File
@@ -0,0 +1,41 @@
export const appConfig = {
name: 'Shake the Frog',
description: 'A fun interactive frog that reacts to shaking!',
url: 'https://shakethefrog.com',
assets: {
favicon: '/images/frog.svg',
ogImage: {
width: 1200,
height: 630,
bgColor: '#c9ffda',
textColor: '#000000'
}
},
skins: {
frog: {
id: 'frog',
name: 'Frog',
normal: '/images/frog.svg',
shaken: '/images/frog-shaken.svg',
isPremium: false
},
mandarin: {
id: 'mandarin',
name: 'Mandarin',
normal: '/images/mandarin.svg',
// TODO: Create a proper shaken version of the mandarin skin
shaken: '/images/mandarin.svg', // Using the same image for both states until a shaken version is created
isPremium: false,
variantId: 'your_mandarin_variant_id_here' // Replace with actual variant ID when created
},
beaver: {
id: 'beaver',
name: 'Beaver',
normal: '/images/beaver.svg',
shaken: '/images/beaver-shaken.svg',
isPremium: true,
variantId: '1047017'
}
},
defaultSkin: 'frog'
} as const
+14
View File
@@ -0,0 +1,14 @@
// Define our curated emoji pool
const emojiPool = [
'💫', '💝', '💘', '💖', '💕',
'💓', '💗', '💞', '✨', '🌟',
'🔥', '👼', '⭐', '💎', '💨',
'🎉', '🕸️', '🤗', '💋', '😘',
'🫂', '👫', '💟', '💌', '🥰',
'😍', '🥺', '😢', '😭'
];
// Helper function to get a random emoji
export function getRandomEmoji(): string {
return emojiPool[Math.floor(Math.random() * emojiPool.length)];
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Server-side feature flag definitions.
*
* Flags are read from environment variables. The abstraction is kept thin
* so a runtime provider (Flipt, Unleash, Flags SDK adapter, etc.) can be
* swapped in later without changing any consumer code.
*
* Convention: FEATURE_<NAME>=1 → enabled
* anything else → disabled
*/
export interface FeatureFlags {
paymentsEnabled: boolean;
}
export function getFeatureFlags(): FeatureFlags {
return {
paymentsEnabled: process.env.FEATURE_PAYMENTS === '1',
};
}
+45
View File
@@ -0,0 +1,45 @@
import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';
// Initialize Lemon Squeezy SDK
export function initializeLemonSqueezy() {
const apiKey = process.env.LEMONSQUEEZY_API_KEY;
if (!apiKey) {
throw new Error('LEMONSQUEEZY_API_KEY is required');
}
lemonSqueezySetup({
apiKey,
onError: (error) => {
throw error; // Fail fast instead of just logging
},
});
}
// Lemon Squeezy configuration with lazy validation.
// Config is only resolved on first access so the module can be safely
// imported even when payment env vars are absent (e.g. payments disabled).
let _config: { storeId: string; webhookSecret: string; baseUrl: string } | null = null;
export function getLemonSqueezyConfig() {
if (_config) return _config;
const storeId = process.env.LEMONSQUEEZY_STORE_ID;
const webhookSecret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
const baseUrl = process.env.NEXT_PUBLIC_APP_URL;
if (!storeId) {
throw new Error('LEMONSQUEEZY_STORE_ID is required');
}
if (!webhookSecret) {
throw new Error('LEMONSQUEEZY_WEBHOOK_SECRET is required');
}
if (!baseUrl) {
throw new Error('NEXT_PUBLIC_APP_URL is required');
}
_config = { storeId, webhookSecret, baseUrl };
return _config;
}
-89
View File
@@ -1,89 +0,0 @@
export const frogMessages = [
"You got me! 🐸",
"Keep shaking! 💫",
"I feel dizzy! 😵‍💫",
"That was fun! ⭐",
"Do it again! 🎉",
"I'm having a blast! 🌟",
"Wheeee! 🎢",
"You're good at this! 🌈",
"I love this game! 💚",
"One more time! ✨",
"That tickles! 😄",
"You found me! 🌿",
"I'm so happy! 🥳",
"Let's party! 🎵",
"You're making me bounce! 💫",
"I'm yours! 💝",
"Shake me harder! 💖",
"Don't stop now! 💕",
"You're amazing! 💗",
"I'm getting hot! 🔥",
"I want more! 💘",
"You're so good! 💓",
"I'm all yours! 💞",
"You drive me wild! 💥",
"I'm melting! 💦",
"I can't resist you! 💋",
"You know what I like! 🌹",
"I'm trembling! ⚡",
"You're irresistible! 💫",
"Make me yours! 💝",
"I'm burning up! 🔥",
"You're making me crazy! 💘",
"I need you! 💖",
"You're perfect! 💕",
"I'm yours forever! 💗",
"Take me! 💫",
"You're incredible! ✨",
"I'm on fire! 🔥",
"You're my everything! 💝",
"I'm in heaven! 💫",
"Your touch is electric! ⚡",
"You make me feel alive! 💖",
"I'm addicted to you! 💕",
"You're my obsession! 💗",
"I can't get enough! 🔥",
"More, more, more! 💘",
"You're my desire! 💓",
"I'm yours to command! 💞",
"Unleash me! 💥",
"You're my fantasy! 💋",
"I crave your touch! 🌹",
"I'm shaking with anticipation! ⚡",
"You're my weakness! 💫",
"Claim me! 💝",
"I'm on the edge! 🔥",
"You're driving me wild! 💘",
"I surrender to you! 💖",
"You're my masterpiece! 💕",
"I'm yours for the taking! 💗",
"Show me what you've got! 💫",
"You're my temptation! ✨",
"I'm consumed by you! 🔥",
"You're my everything and more! 💝",
"I'm lost in you! 💫",
"You're my dream! 💖",
"I'm under your spell! 💕",
"You're my addiction! 💗",
"I'm hooked on you! 🔥",
"Give me all you've got! 💘",
"You're my ultimate fantasy! 💓",
"I'm yours, body and soul! 💞",
"Take me to the edge! 💥",
"I'm overflowing! 💦",
"I yearn for your touch! 🌹",
"I'm quivering with desire! ⚡",
"You're my obsession! 💫",
"Make me yours, completely! 💝",
"I'm a furnace for you! 🔥",
"You're driving me insane! 💘",
"I'm completely yours! 💖",
"You're absolute perfection! 💕",
"I'm yours, now and forever! 💗",
"Take me, I'm yours! 💫",
"You're beyond incredible! ✨",
"I'm a raging inferno! 🔥",
"You're my heart's desire! 💝",
"I'm in paradise! 💫"
];
+100
View File
@@ -0,0 +1,100 @@
import { type Locale } from '../../i18n/request';
// Define grammatical cases for languages that need them
type GrammaticalCase = 'nominative' | 'accusative' | 'dative' | 'genitive' | 'instrumental' | 'prepositional';
// Define which languages need grammatical cases
const languagesWithCases: Partial<Record<Locale, boolean>> = {
ru: true,
ka: true
};
// Localized skin names for different languages with grammatical cases
const skinNames: Record<string, Record<Locale, string | Record<GrammaticalCase, string>>> = {
frog: {
en: 'Frog',
de: 'Frosch',
ru: {
nominative: 'Лягушка',
accusative: 'Лягушку',
dative: 'Лягушке',
genitive: 'Лягушки',
instrumental: 'Лягушкой',
prepositional: 'Лягушке'
},
ka: {
nominative: 'ბაყაყი',
accusative: 'ბაყაყს',
dative: 'ბაყაყს',
genitive: 'ბაყაყის',
instrumental: 'ბაყაყით',
prepositional: 'ბაყაყზე'
},
ar: 'ضفدع'
},
mandarin: {
en: 'Mandarin',
de: 'Mandarine',
ru: {
nominative: 'Мандарин',
accusative: 'Мандарин',
dative: 'Мандарину',
genitive: 'Мандарина',
instrumental: 'Мандарином',
prepositional: 'Мандарине'
},
ka: {
nominative: 'მანდარინი',
accusative: 'მანდარინს',
dative: 'მანდარინს',
genitive: 'მანდარინის',
instrumental: 'მანდარინით',
prepositional: 'მანდარინზე'
},
ar: 'ماندرين'
},
beaver: {
en: 'Beaver',
de: 'Biber',
ru: {
nominative: 'Бобр',
accusative: 'Бобра',
dative: 'Бобру',
genitive: 'Бобра',
instrumental: 'Бобром',
prepositional: 'Бобре'
},
ka: {
nominative: 'თახვი',
accusative: 'თახვს',
dative: 'თახვს',
genitive: 'თახვის',
instrumental: 'თახვით',
prepositional: 'თახვზე'
},
ar: 'قندس'
}
};
/**
* Get the localized name for a skin with the appropriate grammatical case
* @param skinId The skin ID
* @param language The language code
* @param grammaticalCase The grammatical case to use (for languages that need it)
* @returns The localized skin name
*/
export function getLocalizedSkinName(
skinId: string,
language: Locale,
grammaticalCase: GrammaticalCase = 'nominative'
): string {
const skinName = skinNames[skinId]?.[language];
// If the language doesn't use cases or we don't have cases for this skin
if (!skinName || typeof skinName === 'string' || !languagesWithCases[language]) {
return typeof skinName === 'string' ? skinName : skinNames[skinId]?.en as string || skinId;
}
// Return the appropriate case, or fallback to nominative if the case doesn't exist
return skinName[grammaticalCase] || skinName.nominative;
}