Add rewrite
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<NuxtRouteAnnouncer />
|
||||
<UApp :toaster="{ position: 'bottom-center' }">
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
vertical?: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<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 class="font-semibold"><slot name="title" /></div>
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UContainer class="max-w-lg p-8 md:p-16">
|
||||
<UCard>
|
||||
<div class="grid gap-4">
|
||||
<div class="text-center font-bold text-4xl md:text-6xl">
|
||||
{{ error?.statusCode }}
|
||||
</div>
|
||||
<NuxtImg
|
||||
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"
|
||||
/>
|
||||
<UContainer class="max-w-lg p-8 md:p-16">
|
||||
<UCard>
|
||||
<div class="grid gap-4">
|
||||
<div class="text-center font-bold text-4xl md:text-6xl">
|
||||
{{ error?.statusCode }}
|
||||
</div>
|
||||
<NuxtImg
|
||||
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"
|
||||
/>
|
||||
|
||||
<div class="text-center text-lg">
|
||||
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 class="text-center text-lg">
|
||||
Uh oh... Something went super wrong.
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="w-full flex justify-center">
|
||||
<UButton
|
||||
label="Navigate back home"
|
||||
to="/"
|
||||
icon="heroicons:arrow-left"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UContainer>
|
||||
<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>
|
||||
|
||||
<AppFooter />
|
||||
</UApp>
|
||||
<template #footer>
|
||||
<div class="w-full flex justify-center">
|
||||
<UButton
|
||||
label="Navigate back home"
|
||||
to="/"
|
||||
icon="heroicons:arrow-left"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UContainer>
|
||||
|
||||
<AppFooter />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<UApp :toaster="{ position: 'bottom-center' }">
|
||||
<AuthState v-slot="{ loggedIn }">
|
||||
<Register v-if="!loggedIn" />
|
||||
<template v-if="loggedIn">
|
||||
|
||||
@@ -110,6 +110,8 @@ const saveOp = watchPausable(
|
||||
</UCard>
|
||||
</UContainer>
|
||||
|
||||
<RewriteCard />
|
||||
|
||||
<UContainer class="max-w-2xl py-8 flex flex-col gap-4">
|
||||
<div class="font-bold text-lg">🪪 Your connected Strava account</div>
|
||||
<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",
|
||||
"@vee-validate/nuxt": "^4.15.0",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"destr": "^2.0.5",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"nuxt": "^3.16.2",
|
||||
"nuxt-auth-utils": "0.5.18",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@nuxt/ui": "3.0.2",
|
||||
"@vee-validate/nuxt": "^4.15.0",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"destr": "^2.0.5",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"nuxt": "^3.16.2",
|
||||
"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;
|
||||
weight: number;
|
||||
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,
|
||||
"tag": "0000_slim_blonde_phantom",
|
||||
"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,
|
||||
timestamp,
|
||||
jsonb,
|
||||
boolean,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
@@ -15,6 +16,9 @@ export const users = pgTable("users", {
|
||||
city: text("city"),
|
||||
country: text("country"),
|
||||
sex: text("sex"),
|
||||
premium: boolean("premium")
|
||||
.notNull()
|
||||
.$defaultFn(() => false),
|
||||
weight: numeric("weight", {
|
||||
mode: "number",
|
||||
}),
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineOAuthStravaEventHandler({
|
||||
sex: auth.user.sex,
|
||||
weight: auth.user.weight,
|
||||
avatar: auth.user.profile,
|
||||
premium: false,
|
||||
};
|
||||
|
||||
const db = useDrizzle();
|
||||
@@ -41,7 +42,7 @@ export default defineOAuthStravaEventHandler({
|
||||
.values(userPayload)
|
||||
.onConflictDoUpdate({
|
||||
target: tables.users.id,
|
||||
set: omit(userPayload, ["id"]),
|
||||
set: omit(userPayload, ["id", "premium"]),
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -77,7 +78,7 @@ export default defineOAuthStravaEventHandler({
|
||||
.onConflictDoNothing();
|
||||
|
||||
await setUserSession(event, {
|
||||
user: userPayload,
|
||||
user: { ...userPayload, premium: user.premium },
|
||||
});
|
||||
|
||||
sendRedirect(event, "/");
|
||||
|
||||
@@ -20,7 +20,10 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
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) {
|
||||
throw createError({
|
||||
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}`, {
|
||||
method: "PUT",
|
||||
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";
|
||||
|
||||
const promo = "Written by https://ghostwriter.rocks 👻";
|
||||
|
||||
type Activity = Record<string, any>;
|
||||
|
||||
const movingActivityTypes = [
|
||||
@@ -116,7 +119,13 @@ export const createActivityContent = async (
|
||||
"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 = `
|
||||
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