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
5 changes: 1 addition & 4 deletions .zed/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
},
"default_profile": "write"
},
"features": {
"edit_prediction_provider": "copilot"
},
"language_servers": ["!eslint", "..."],
"format_on_save": "off",
"format_on_save": "on",
"autosave": {
"after_delay": {
"milliseconds": 1000
Expand Down
19 changes: 19 additions & 0 deletions apps/app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# Application
#

NUXT_PUBLIC_BASE_URL=http://localhost:4000

#
# Supabase
#

SUPABASE_URL=
SUPABASE_KEY=

#
# Posthog
#

NUXT_PUBLIC_POSTHOG_KEY=phc_Vu3wqbMj8M5iWzvEulIXlK5U0A3hqkb5rlVM1DLtZsb
NUXT_PUBLIC_POSTHOG_HOST=
24 changes: 24 additions & 0 deletions apps/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
1 change: 1 addition & 0 deletions apps/app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @styleframe/app
21 changes: 21 additions & 0 deletions apps/app/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Alex Grozav

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions apps/app/app/api/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { SupabaseClient } from "@supabase/supabase-js";

export interface ActivateLicenseRequest {
label: string;
key: string;
}

export interface ActivateLicenseResponse {
success: boolean;
message?: string;
error?: string;
}

export async function activateLicense(
supabase: SupabaseClient,
data: ActivateLicenseRequest,
): Promise<ActivateLicenseResponse> {
try {
const { data: response, error } =
await supabase.functions.invoke<ActivateLicenseResponse>(
"activate-license",
{
body: {
label: data.label,
key: data.key,
},
},
);

if (error) {
return {
success: false,
error: error.message || "Failed to activate license",
};
}

return response || { success: false, error: "No response from server" };
} catch (err) {
return {
success: false,
error:
err instanceof Error ? err.message : "An unexpected error occurred",
};
}
}
52 changes: 52 additions & 0 deletions apps/app/app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import * as nuxtUiLocales from "@nuxt/ui/locale";

const { seo } = useAppConfig();
const site = useSiteConfig();
const { locale, locales, isEnabled, switchLocalePath } = useAppI18n();

const lang = computed(
() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.code || "en",
);
const dir = computed(
() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.dir || "ltr",
);

useHead({
meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
link: [{ rel: "icon", href: "/favicon.ico" }],
htmlAttrs: {
lang,
dir,
},
});

useSeoMeta({
titleTemplate: seo.titleTemplate,
title: seo.title,
description: seo.description,
ogSiteName: site.name,
twitterCard: "summary_large_image",
});

if (isEnabled.value) {
const route = useRoute();
const defaultLocale = useRuntimeConfig().public.i18n.defaultLocale!;
onMounted(() => {
const currentLocale = route.path.split("/")[1];
if (!locales.some((locale) => locale.code === currentLocale)) {
return navigateTo(switchLocalePath(defaultLocale) as string);
}
});
}
</script>

<template>
<UApp :locale="nuxtUiLocales[locale as keyof typeof nuxtUiLocales]">
<NuxtLoadingIndicator color="var(--ui-primary)" />

<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
79 changes: 79 additions & 0 deletions apps/app/app/components/IconMenuToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { motion } from "motion-v";
import type { VariantType } from "motion-v";

const props = defineProps<{
open: boolean;
}>();

const variants: {
[k: string]: VariantType | ((custom: unknown) => VariantType);
} = {
normal: {
rotate: 0,
y: 0,
opacity: 1,
},
close: (custom: unknown) => {
const c = custom as number;
return {
rotate: c === 1 ? 45 : c === 3 ? -45 : 0,
y: c === 1 ? 6 : c === 3 ? -6 : 0,
opacity: c === 2 ? 0 : 1,
transition: {
type: "spring",
stiffness: 260,
damping: 20,
},
};
},
};

const state = computed(() => (props.open ? "close" : "normal"));
</script>

<template>
<UButton size="sm" variant="ghost" color="neutral" class="-me-1.5" square>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<motion.line
x1="4"
y1="6"
x2="20"
y2="6"
:variants="variants"
:animate="state"
:custom="1"
class="outline-none"
/>
<motion.line
x1="4"
y1="12"
x2="20"
y2="12"
:variants="variants"
:animate="state"
:custom="2"
class="outline-none"
/>
<motion.line
x1="4"
y1="18"
x2="20"
y2="18"
:variants="variants"
:animate="state"
:custom="3"
class="outline-none"
/>
</svg>
</UButton>
</template>
74 changes: 74 additions & 0 deletions apps/app/app/components/LanguageSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
const { locale, locales, switchLocalePath } = useAppI18n();

function getEmojiFlag(locale: string): string {
const languageToCountry: Record<string, string> = {
ar: "sa", // Arabic -> Saudi Arabia
bn: "bd", // Bengali -> Bangladesh
ca: "es", // Catalan -> Spain
ckb: "iq", // Central Kurdish -> Iraq
cs: "cz", // Czech -> Czech Republic (note: modern country code is actually 'cz')
da: "dk", // Danish -> Denmark
el: "gr", // Greek -> Greece
en: "gb", // English -> Great Britain
et: "ee", // Estonian -> Estonia
he: "il", // Hebrew -> Israel
hi: "in", // Hindi -> India
hy: "am", // Armenian -> Armenia
ja: "jp", // Japanese -> Japan
kk: "kz", // Kazakh -> Kazakhstan
km: "kh", // Khmer -> Cambodia
ko: "kr", // Korean -> South Korea
ky: "kg", // Kyrgyz -> Kyrgyzstan
lb: "lu", // Luxembourgish -> Luxembourg
ms: "my", // Malay -> Malaysia
nb: "no", // Norwegian Bokmål -> Norway
sl: "si", // Slovenian -> Slovenia
sv: "se", // Swedish -> Sweden
uk: "ua", // Ukrainian -> Ukraine
ur: "pk", // Urdu -> Pakistan
vi: "vn", // Vietnamese -> Vietnam
};

const baseLanguage = locale.split("-")[0]?.toLowerCase() || locale;
const countryCode =
languageToCountry[baseLanguage] || locale.replace(/^.*-/, "").slice(0, 2);

return countryCode
.toUpperCase()
.split("")
.map((char) => String.fromCodePoint(0x1f1a5 + char.charCodeAt(0)))
.join("");
}
</script>

<template>
<UPopover mode="hover" :content="{ align: 'end' }">
<UButton color="neutral" variant="ghost" class="size-8">
<template #trailing>
<span class="text-lg">
{{ getEmojiFlag(locale) }}
</span>
</template>
</UButton>

<template #content>
<ul class="flex flex-col">
<li v-for="localeItem in locales" :key="localeItem.code">
<NuxtLink
class="flex justify-between py-1.5 px-2 gap-1 hover:bg-muted"
:to="switchLocalePath(localeItem.code) as string"
:aria-label="localeItem.name"
>
<span class="text-sm">
{{ localeItem.name }}
</span>
<span class="size-5 text-center">
{{ getEmojiFlag(localeItem.code) }}
</span>
</NuxtLink>
</li>
</ul>
</template>
</UPopover>
</template>
Loading
Loading