Amend app layout

This commit is contained in:
2025-05-22 14:04:16 +03:00
parent 16a4ea0949
commit e080f3978c
13 changed files with 8 additions and 0 deletions

22
components/app-bar.vue Normal file
View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
const { user, clear } = useUserSession();
</script>
<template>
<div class="w-full h-16 border-b-4 border-b-orange-500 flex">
<UContainer class="max-w-2xl flex justify-between items-center">
<div class="flex gap-2 items-center">
<NuxtImg src="/ghostwriter-logo.png" class="size-9" />
<div class="font-bold text-xl tracking-tight font-fira-code">
Ghostwriter
</div>
</div>
<UDropdownMenu :items="[{ label: 'Log out', onSelect: () => clear() }]">
<UAvatar
:src="user!.avatar"
class="border border-gray-200 cursor-pointer"
/>
</UDropdownMenu>
</UContainer>
</div>
</template>

13
components/app-footer.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<UContainer
class="max-w-2xl py-8 text-sm text-slate-600 place-items-center grid gap-4"
>
<div class="text-center">
Built with 💪 in Athens, Greece 🇬🇷 by Marios Antonoudiou.
<ULink class="underline" href="mailto:mariosant@sent.com"
>Send feedback</ULink
>.
</div>
<NuxtImg src="/images/powered-by-strava.svg" width="80px" />
</UContainer>
</template>

35
components/card-field.vue Normal file
View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
interface Props {
vertical?: boolean;
}
const props = defineProps<Props>();
</script>
<template>
<div
v-if="props.vertical"
class="flex justify-between items-start gap-2 flex-col"
>
<div>
<div class="font-semibold"><slot name="title" /></div>
<div class="text-slate-600 text-sm hidden md:block">
<slot name="description" />
</div>
</div>
<div class="text-nowrap w-full">
<slot name="value" />
</div>
</div>
<div v-else class="flex justify-between items-center gap-4">
<div>
<div class="font-semibold"><slot name="title" /></div>
<div class="text-slate-600 text-sm hidden md:block">
<slot name="description" />
</div>
</div>
<div class="text-nowrap flex items-end">
<slot name="value" />
</div>
</div>
</template>

52
components/register.vue Normal file
View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import AppFooter from "./app-footer.vue";
useHead({ title: "Ghostwriter - Please sign in" });
const { openInPopup } = useUserSession();
</script>
<template>
<UContainer
class="flex flex-col items-center gap-4 justify-center w-full p-16"
>
<UCard class="max-w-sm grid gap-6 justify-center items-center">
<div class="flex flex-col gap-10 items-center justify-center">
<div class="flex gap-2 items-center">
<NuxtImg src="/ghostwriter-logo.png" class="size-9" />
<div class="font-bold text-xl tracking-tight font-fira-code">
Ghostwriter
</div>
</div>
<div class="grid gap-2">
<div class="text-center text-2xl font-bold">
Sign in to your account.
</div>
<div class="text-center">
Connect with Strava to automatically add personalized titles and
descriptions to your activities.
</div>
</div>
<div
aria-role="button"
@click="openInPopup('/auth/strava')"
class="cursor-pointer w-full max-w-[200px]"
>
<NuxtImg
src="/images/connect-with-strava.svg"
class="w-full max-w-[200px]"
/>
</div>
<div class="text-sm text-gray-500">
By signing in, you agree to our
<NuxtLink
href="https://www.ghostwriter.rocks/privacy"
class="text-primary-500"
>Privacy Policy</NuxtLink
>.
</div>
</div>
</UCard>
</UContainer>
<AppFooter />
</template>

122
components/rewrite-card.vue Normal file
View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import type { FormSubmitEvent } from "@nuxt/ui";
const { user } = useUserSession();
const toast = useToast();
const form = useTemplateRef("form");
const submitting = ref(false);
const formData = {
activityUrl: "",
};
const validate = ({
activityUrl,
}: Partial<{
activityUrl: string;
}>) => {
if (
[
"https://strava.com/activities/",
"https://www.strava.com/activities/",
].some((u) => activityUrl!.includes(u))
) {
return [];
}
return [
{
name: "activityUrl",
message: "Please write a legit Strava activity URL.",
},
];
};
const submit = async (event: FormSubmitEvent<typeof formData>) => {
await $fetch("/api/rewrite", {
method: "POST",
query: {
activity: event.data.activityUrl,
},
})
.then(() =>
toast.add({
title: "Success",
description: "Activity has been rewritten.",
color: "success",
}),
)
.catch(() =>
toast.add({
title: "Error",
description: "Something wrong has happened. Maybe try again later.",
color: "error",
}),
)
.finally(() => {
formData.activityUrl = "";
});
};
</script>
<template>
<UForm
:disabled="!user.premium"
ref="form"
@submit="submit"
:validate="validate"
:state="formData"
class="flex flex-col gap-8"
>
<template v-slot:default="errors">
<UContainer class="max-w-2xl py-8 flex flex-col gap-4">
<div class="flex justify-between items-center">
<div class="font-bold text-lg">🔄 Re-write activity</div>
<UTooltip
arrow
:disabled="user.premium"
text="This feature is enabled for premium users. You can upgrade to
premium by supporting Ghostwriter."
>
<UBadge variant="soft">Premium only</UBadge>
</UTooltip>
</div>
<UCard>
<div class="grid gap-4">
<CardField vertical>
<template #title> Activity URL </template>
<template #description>
Paste your Strava activity URL to rewrite it.
</template>
<template #value>
<div class="grid gap-2">
<UInput
:disabled="submitting"
v-model="formData.activityUrl"
class="w-full"
placeholder="https://www.strava.com/activities/12345678901"
/>
<div class="text-error-500 text-sm">
{{ errors?.errors[0]?.message }}
</div>
</div>
</template>
</CardField>
</div>
<template #footer>
<UButton
:disabled="!user.premium"
loading-auto
label="Rewrite"
color="neutral"
variant="soft"
@click="form!.submit()"
/>
</template>
</UCard>
</UContainer>
</template>
</UForm>
</template>