Add rewrite
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtRouteAnnouncer />
|
||||||
<UApp :toaster="{ position: 'bottom-center' }">
|
<NuxtLayout>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
</NuxtLayout>
|
||||||
</NuxtLayout>
|
|
||||||
</UApp>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
vertical?: boolean;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between items-center gap-4">
|
<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>
|
||||||
<div class="font-semibold"><slot name="title" /></div>
|
<div class="font-semibold"><slot name="title" /></div>
|
||||||
<div class="text-slate-600 text-sm hidden md:block">
|
<div class="text-slate-600 text-sm hidden md:block">
|
||||||
|
|||||||
122
app/components/rewrite-card.vue
Normal file
122
app/components/rewrite-card.vue
Normal 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>
|
||||||
@@ -3,41 +3,39 @@ const error = useError();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UApp>
|
<UContainer class="max-w-lg p-8 md:p-16">
|
||||||
<UContainer class="max-w-lg p-8 md:p-16">
|
<UCard>
|
||||||
<UCard>
|
<div class="grid gap-4">
|
||||||
<div class="grid gap-4">
|
<div class="text-center font-bold text-4xl md:text-6xl">
|
||||||
<div class="text-center font-bold text-4xl md:text-6xl">
|
{{ error?.statusCode }}
|
||||||
{{ error?.statusCode }}
|
</div>
|
||||||
</div>
|
<NuxtImg
|
||||||
<NuxtImg
|
class="rounded"
|
||||||
class="rounded"
|
src="https://plus.unsplash.com/premium_photo-1738036169480-086c0030803c?q=80&w=512&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||||
src="https://plus.unsplash.com/premium_photo-1738036169480-086c0030803c?q=80&w=512&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="text-center text-lg">
|
<div class="text-center text-lg">
|
||||||
Uh oh... Something went super wrong.
|
Uh oh... Something went super wrong.
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text-center font-mono p-4 bg-neutral-100 text-sm dark:bg-neutral-800 max-h-32 overflow-y-auto wrap-normal relative"
|
|
||||||
>
|
|
||||||
{{ error?.message }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<div
|
||||||
<div class="w-full flex justify-center">
|
class="text-center font-mono p-4 bg-neutral-100 text-sm dark:bg-neutral-800 max-h-32 overflow-y-auto wrap-normal relative"
|
||||||
<UButton
|
>
|
||||||
label="Navigate back home"
|
{{ error?.message }}
|
||||||
to="/"
|
</div>
|
||||||
icon="heroicons:arrow-left"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
</UContainer>
|
|
||||||
|
|
||||||
<AppFooter />
|
<template #footer>
|
||||||
</UApp>
|
<div class="w-full flex justify-center">
|
||||||
|
<UButton
|
||||||
|
label="Navigate back home"
|
||||||
|
to="/"
|
||||||
|
icon="heroicons:arrow-left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</UContainer>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<UApp>
|
<UApp :toaster="{ position: 'bottom-center' }">
|
||||||
<AuthState v-slot="{ loggedIn }">
|
<AuthState v-slot="{ loggedIn }">
|
||||||
<Register v-if="!loggedIn" />
|
<Register v-if="!loggedIn" />
|
||||||
<template v-if="loggedIn">
|
<template v-if="loggedIn">
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ const saveOp = watchPausable(
|
|||||||
</UCard>
|
</UCard>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
|
|
||||||
|
<RewriteCard />
|
||||||
|
|
||||||
<UContainer class="max-w-2xl py-8 flex flex-col gap-4">
|
<UContainer class="max-w-2xl py-8 flex flex-col gap-4">
|
||||||
<div class="font-bold text-lg">🪪 Your connected Strava account</div>
|
<div class="font-bold text-lg">🪪 Your connected Strava account</div>
|
||||||
<UCard class="bg-neutral-50 dark:bg-slate-800">
|
<UCard class="bg-neutral-50 dark:bg-slate-800">
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@nuxt/ui": "3.0.2",
|
"@nuxt/ui": "3.0.2",
|
||||||
"@vee-validate/nuxt": "^4.15.0",
|
"@vee-validate/nuxt": "^4.15.0",
|
||||||
"@vueuse/nuxt": "^13.0.0",
|
"@vueuse/nuxt": "^13.0.0",
|
||||||
|
"destr": "^2.0.5",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.41.0",
|
||||||
"nuxt": "^3.16.2",
|
"nuxt": "^3.16.2",
|
||||||
"nuxt-auth-utils": "0.5.18",
|
"nuxt-auth-utils": "0.5.18",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@nuxt/ui": "3.0.2",
|
"@nuxt/ui": "3.0.2",
|
||||||
"@vee-validate/nuxt": "^4.15.0",
|
"@vee-validate/nuxt": "^4.15.0",
|
||||||
"@vueuse/nuxt": "^13.0.0",
|
"@vueuse/nuxt": "^13.0.0",
|
||||||
|
"destr": "^2.0.5",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.41.0",
|
||||||
"nuxt": "^3.16.2",
|
"nuxt": "^3.16.2",
|
||||||
"nuxt-auth-utils": "0.5.18",
|
"nuxt-auth-utils": "0.5.18",
|
||||||
|
|||||||
53
server/api/rewrite.post.ts
Normal file
53
server/api/rewrite.post.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const session = await requireUserSession(event);
|
||||||
|
|
||||||
|
const db = useDrizzle();
|
||||||
|
|
||||||
|
const query = getQuery(event);
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: (f, o) => o.eq(f.id, session.user.id),
|
||||||
|
with: {
|
||||||
|
preferences: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.premium) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Premium membership required.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityId = (query.activity as string).replace(
|
||||||
|
/https:\/\/(www\.)?strava\.com\/activities\//,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const strava = await useStrava(session.user.id);
|
||||||
|
|
||||||
|
const activity = await strava!<any>(`/activities/${activityId}`);
|
||||||
|
|
||||||
|
const [aiError, stravaRequestBody] = await createActivityContent(
|
||||||
|
activity,
|
||||||
|
user!,
|
||||||
|
);
|
||||||
|
if (aiError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: `OPENAI API: ${aiError.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await strava!(`activities/${activityId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: stravaRequestBody,
|
||||||
|
}).catch((error) => {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: `Strava API: ${error.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sendNoContent(event);
|
||||||
|
});
|
||||||
1
server/auth.d.ts
vendored
1
server/auth.d.ts
vendored
@@ -16,6 +16,7 @@ declare module "#auth-utils" {
|
|||||||
sex: string;
|
sex: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
premium: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
server/database/migrations/0001_smooth_jazinda.sql
Normal file
1
server/database/migrations/0001_smooth_jazinda.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "users" ADD COLUMN "premium" boolean;
|
||||||
1
server/database/migrations/0002_many_marauders.sql
Normal file
1
server/database/migrations/0002_many_marauders.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "users" ALTER COLUMN "premium" SET NOT NULL;
|
||||||
228
server/database/migrations/meta/0001_snapshot.json
Normal file
228
server/database/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
"id": "912c6e36-57b1-4e7f-a29b-586b187b1c32",
|
||||||
|
"prevId": "c8519a52-b999-48f3-b532-42a5678e3905",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.preferences": {
|
||||||
|
"name": "preferences",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "preferences_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "data",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preferences_user_id_users_id_fk": {
|
||||||
|
"name": "preferences_user_id_users_id_fk",
|
||||||
|
"tableFrom": "preferences",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"preferences_user_id_unique": {
|
||||||
|
"name": "preferences_user_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.tokens": {
|
||||||
|
"name": "tokens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "tokens_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tokens_user_id_users_id_fk": {
|
||||||
|
"name": "tokens_user_id_users_id_fk",
|
||||||
|
"tableFrom": "tokens",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"tokens_user_id_unique": {
|
||||||
|
"name": "tokens_user_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"name": "city",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"sex": {
|
||||||
|
"name": "sex",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"premium": {
|
||||||
|
"name": "premium",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"name": "weight",
|
||||||
|
"type": "numeric",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
228
server/database/migrations/meta/0002_snapshot.json
Normal file
228
server/database/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
"id": "7b607167-551b-4aa3-8305-afe3c4d72ace",
|
||||||
|
"prevId": "912c6e36-57b1-4e7f-a29b-586b187b1c32",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.preferences": {
|
||||||
|
"name": "preferences",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "preferences_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "data",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preferences_user_id_users_id_fk": {
|
||||||
|
"name": "preferences_user_id_users_id_fk",
|
||||||
|
"tableFrom": "preferences",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"preferences_user_id_unique": {
|
||||||
|
"name": "preferences_user_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.tokens": {
|
||||||
|
"name": "tokens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "tokens_id_seq",
|
||||||
|
"schema": "public",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tokens_user_id_users_id_fk": {
|
||||||
|
"name": "tokens_user_id_users_id_fk",
|
||||||
|
"tableFrom": "tokens",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"tokens_user_id_unique": {
|
||||||
|
"name": "tokens_user_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"name": "city",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"sex": {
|
||||||
|
"name": "sex",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"premium": {
|
||||||
|
"name": "premium",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"name": "weight",
|
||||||
|
"type": "numeric",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,20 @@
|
|||||||
"when": 1745335305323,
|
"when": 1745335305323,
|
||||||
"tag": "0000_slim_blonde_phantom",
|
"tag": "0000_slim_blonde_phantom",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1747547391071,
|
||||||
|
"tag": "0001_smooth_jazinda",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1747564780270,
|
||||||
|
"tag": "0002_many_marauders",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
numeric,
|
numeric,
|
||||||
timestamp,
|
timestamp,
|
||||||
jsonb,
|
jsonb,
|
||||||
|
boolean,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const users = pgTable("users", {
|
export const users = pgTable("users", {
|
||||||
@@ -15,6 +16,9 @@ export const users = pgTable("users", {
|
|||||||
city: text("city"),
|
city: text("city"),
|
||||||
country: text("country"),
|
country: text("country"),
|
||||||
sex: text("sex"),
|
sex: text("sex"),
|
||||||
|
premium: boolean("premium")
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => false),
|
||||||
weight: numeric("weight", {
|
weight: numeric("weight", {
|
||||||
mode: "number",
|
mode: "number",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default defineOAuthStravaEventHandler({
|
|||||||
sex: auth.user.sex,
|
sex: auth.user.sex,
|
||||||
weight: auth.user.weight,
|
weight: auth.user.weight,
|
||||||
avatar: auth.user.profile,
|
avatar: auth.user.profile,
|
||||||
|
premium: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const db = useDrizzle();
|
const db = useDrizzle();
|
||||||
@@ -41,7 +42,7 @@ export default defineOAuthStravaEventHandler({
|
|||||||
.values(userPayload)
|
.values(userPayload)
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: tables.users.id,
|
target: tables.users.id,
|
||||||
set: omit(userPayload, ["id"]),
|
set: omit(userPayload, ["id", "premium"]),
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ export default defineOAuthStravaEventHandler({
|
|||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
await setUserSession(event, {
|
await setUserSession(event, {
|
||||||
user: userPayload,
|
user: { ...userPayload, premium: user.premium },
|
||||||
});
|
});
|
||||||
|
|
||||||
sendRedirect(event, "/");
|
sendRedirect(event, "/");
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const activity = await strava!<any>(`/activities/${body.object_id}`);
|
const activity = await strava!<any>(`/activities/${body.object_id}`);
|
||||||
|
|
||||||
const [aiError, aiResponse] = await createActivityContent(activity, user);
|
const [aiError, stravaRequestBody] = await createActivityContent(
|
||||||
|
activity,
|
||||||
|
user,
|
||||||
|
);
|
||||||
if (aiError) {
|
if (aiError) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -28,20 +31,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseObject = JSON.parse(
|
|
||||||
get(aiResponse, "output.0.content.0.text"),
|
|
||||||
) as {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const promo = "Written by https://ghostwriter.rocks 👻";
|
|
||||||
|
|
||||||
const stravaRequestBody = {
|
|
||||||
name: responseObject.title,
|
|
||||||
description: [responseObject.description, promo].join("\n"),
|
|
||||||
};
|
|
||||||
|
|
||||||
await strava!(`activities/${body.object_id}`, {
|
await strava!(`activities/${body.object_id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: stravaRequestBody,
|
body: stravaRequestBody,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { chain, draw, omit } from "radash";
|
import { chain, draw, get, omit, tryit } from "radash";
|
||||||
|
import { safeDestr } from "destr";
|
||||||
import { User } from "./drizzle";
|
import { User } from "./drizzle";
|
||||||
|
|
||||||
|
const promo = "Written by https://ghostwriter.rocks 👻";
|
||||||
|
|
||||||
type Activity = Record<string, any>;
|
type Activity = Record<string, any>;
|
||||||
|
|
||||||
const movingActivityTypes = [
|
const movingActivityTypes = [
|
||||||
@@ -116,7 +119,13 @@ export const createActivityContent = async (
|
|||||||
"snarky",
|
"snarky",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const length = draw(["short", "short", "short", "medium", "a-little-more-than-medium"]);
|
const length = draw([
|
||||||
|
"short",
|
||||||
|
"short",
|
||||||
|
"short",
|
||||||
|
"medium",
|
||||||
|
"a-little-more-than-medium",
|
||||||
|
]);
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
Generate a short title and a ${length}-lengthed description for my strava activity. Use my preferred language and unit system.
|
Generate a short title and a ${length}-lengthed description for my strava activity. Use my preferred language and unit system.
|
||||||
@@ -168,5 +177,17 @@ export const createActivityContent = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return [aiError, aiResponse] as const;
|
const [parseError, responseObject] = tryit(
|
||||||
|
chain(
|
||||||
|
(r) => get(r, "output.0.content.0.text"),
|
||||||
|
(r) => safeDestr<{ title: string; description: string }>(r),
|
||||||
|
),
|
||||||
|
)(aiResponse);
|
||||||
|
|
||||||
|
const stravaRequestBody = {
|
||||||
|
name: responseObject!.title,
|
||||||
|
description: [responseObject!.description, promo].join("\n"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [aiError || parseError, stravaRequestBody] as const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user