diff --git a/web/app/actions.ts b/web/app/actions.ts
index 8f3fff17..a2d7926a 100644
--- a/web/app/actions.ts
+++ b/web/app/actions.ts
@@ -1,28 +1,5 @@
-import { clerkClient, getAuth } from "@clerk/tanstack-start/server"
import { createServerFn } from "@tanstack/start"
-import { create, get } from "ronin"
-import * as cheerio from "cheerio"
-import { ensureUrlProtocol } from "@/lib/utils"
-import { urlSchema } from "@/lib/utils/schema"
-
-interface Metadata {
- title: string
- description: string
- icon: string | null
- url: string
-}
-
-const DEFAULT_VALUES = {
- TITLE: "",
- DESCRIPTION: "",
- FAVICON: null,
-}
-
-export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
- const auth = await getAuth(ctx.request)
-
- return auth
-})
+import { get } from "ronin"
export const getFeatureFlag = createServerFn(
"GET",
@@ -34,84 +11,3 @@ export const getFeatureFlag = createServerFn(
return response
},
)
-
-export const sendFeedbackFn = createServerFn(
- "POST",
- async (data: { content: string }, { request }) => {
- const auth = await getAuth(request)
- if (!auth.userId) {
- throw new Error("Unauthorized")
- }
- const user = await clerkClient({
- telemetry: { disabled: true },
- }).users.getUser(auth.userId)
- await create.feedback.with({
- message: data.content,
- emailFrom: user.emailAddresses[0].emailAddress,
- })
- },
-)
-
-export const getMetadata = createServerFn("GET", async (url: string) => {
- if (!url) {
- return new Response('Missing "url" query parameter', {
- status: 400,
- })
- }
-
- const result = urlSchema.safeParse(url)
- if (!result.success) {
- throw new Error(
- result.error.issues.map((issue) => issue.message).join(", "),
- )
- }
-
- url = ensureUrlProtocol(url)
-
- try {
- const response = await fetch(url, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
- },
- })
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
-
- const data = await response.text()
- const $ = cheerio.load(data)
-
- const metadata: Metadata = {
- title:
- $("title").text() ||
- $('meta[property="og:title"]').attr("content") ||
- DEFAULT_VALUES.TITLE,
- description:
- $('meta[name="description"]').attr("content") ||
- $('meta[property="og:description"]').attr("content") ||
- DEFAULT_VALUES.DESCRIPTION,
- icon:
- $('link[rel="icon"]').attr("href") ||
- $('link[rel="shortcut icon"]').attr("href") ||
- DEFAULT_VALUES.FAVICON,
- url: url,
- }
-
- if (metadata.icon && !metadata.icon.startsWith("http")) {
- metadata.icon = new URL(metadata.icon, url).toString()
- }
-
- return metadata
- } catch (error) {
- console.error("Error fetching metadata:", error)
- const defaultMetadata: Metadata = {
- title: DEFAULT_VALUES.TITLE,
- description: DEFAULT_VALUES.DESCRIPTION,
- icon: DEFAULT_VALUES.FAVICON,
- url: url,
- }
- return defaultMetadata
- }
-})
diff --git a/web/app/components/sidebar/partials/feedback.tsx b/web/app/components/sidebar/partials/feedback.tsx
index ebdb093e..ec2db9c9 100644
--- a/web/app/components/sidebar/partials/feedback.tsx
+++ b/web/app/components/sidebar/partials/feedback.tsx
@@ -29,7 +29,28 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { toast } from "sonner"
import { Spinner } from "@/components/custom/spinner"
import { Editor } from "@tiptap/react"
-import { sendFeedbackFn } from "~/actions"
+import { createServerFn } from "@tanstack/start"
+import { clerkClient, getAuth } from "@clerk/tanstack-start/server"
+import { create } from "ronin"
+
+export const sendFeedbackFn = createServerFn(
+ "POST",
+ async (data: { content: string }, { request }) => {
+ const auth = await getAuth(request)
+
+ if (!auth.userId) {
+ throw new Error("User not authenticated")
+ }
+
+ const user = await clerkClient({
+ telemetry: { disabled: true },
+ }).users.getUser(auth.userId)
+ await create.feedback.with({
+ message: data.content,
+ emailFrom: user.emailAddresses[0].emailAddress,
+ })
+ },
+)
const formSchema = z.object({
content: z.string().min(1, {
diff --git a/web/app/routes/__root.tsx b/web/app/routes/__root.tsx
index 110e6aa2..65f27489 100644
--- a/web/app/routes/__root.tsx
+++ b/web/app/routes/__root.tsx
@@ -1,13 +1,20 @@
///
+import { getAuth } from "@clerk/tanstack-start/server"
import type { QueryClient } from "@tanstack/react-query"
import {
Outlet,
ScrollRestoration,
createRootRouteWithContext,
} from "@tanstack/react-router"
-import { Body, Head, Html, Meta, Scripts } from "@tanstack/start"
+import {
+ Body,
+ createServerFn,
+ Head,
+ Html,
+ Meta,
+ Scripts,
+} from "@tanstack/start"
import * as React from "react"
-import { fetchClerkAuth } from "~/actions"
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary.js"
import { NotFound } from "~/components/NotFound.js"
import appCss from "~/styles/app.css?url"
@@ -25,11 +32,17 @@ export const ReactQueryDevtools =
process.env.NODE_ENV === "production"
? () => null
: React.lazy(() =>
- import("@tanstack/react-query-devtools/production").then((d) => ({
+ import("@tanstack/react-query-devtools").then((d) => ({
default: d.ReactQueryDevtools,
})),
)
+export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
+ const auth = await getAuth(ctx.request)
+
+ return auth
+})
+
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
diff --git a/web/app/routes/_layout/_pages/_protected/links/-link-form.tsx b/web/app/routes/_layout/_pages/_protected/links/-link-form.tsx
index 4deedd30..5e54be51 100644
--- a/web/app/routes/_layout/_pages/_protected/links/-link-form.tsx
+++ b/web/app/routes/_layout/_pages/_protected/links/-link-form.tsx
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { toast } from "sonner"
import { createLinkSchema, LinkFormValues } from "./-schema"
-import { cn, generateUniqueSlug } from "@/lib/utils"
+import { cn, ensureUrlProtocol, generateUniqueSlug } from "@/lib/utils"
import { Form } from "@/components/ui/form"
import { Button } from "@/components/ui/button"
import { atom, useAtom } from "jotai"
@@ -22,12 +22,91 @@ import { useOnClickOutside } from "~/hooks/use-on-click-outside"
import TopicSelector, {
topicSelectorAtom,
} from "~/components/custom/topic-selector"
-import { getMetadata } from "~/actions"
+import { createServerFn } from "@tanstack/start"
+import { urlSchema } from "~/lib/utils/schema"
+import * as cheerio from "cheerio"
+
+interface Metadata {
+ title: string
+ description: string
+ icon: string | null
+ url: string
+}
+
+const DEFAULT_VALUES = {
+ TITLE: "",
+ DESCRIPTION: "",
+ FAVICON: null,
+}
export const globalLinkFormExceptionRefsAtom = atom<
React.RefObject[]
>([])
+export const getMetadata = createServerFn("GET", async (url: string) => {
+ if (!url) {
+ return new Response('Missing "url" query parameter', {
+ status: 400,
+ })
+ }
+
+ const result = urlSchema.safeParse(url)
+ if (!result.success) {
+ throw new Error(
+ result.error.issues.map((issue) => issue.message).join(", "),
+ )
+ }
+
+ url = ensureUrlProtocol(url)
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ },
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const data = await response.text()
+ const $ = cheerio.load(data)
+
+ const metadata: Metadata = {
+ title:
+ $("title").text() ||
+ $('meta[property="og:title"]').attr("content") ||
+ DEFAULT_VALUES.TITLE,
+ description:
+ $('meta[name="description"]').attr("content") ||
+ $('meta[property="og:description"]').attr("content") ||
+ DEFAULT_VALUES.DESCRIPTION,
+ icon:
+ $('link[rel="icon"]').attr("href") ||
+ $('link[rel="shortcut icon"]').attr("href") ||
+ DEFAULT_VALUES.FAVICON,
+ url: url,
+ }
+
+ if (metadata.icon && !metadata.icon.startsWith("http")) {
+ metadata.icon = new URL(metadata.icon, url).toString()
+ }
+
+ return metadata
+ } catch (error) {
+ console.error("Error fetching metadata:", error)
+ const defaultMetadata: Metadata = {
+ title: DEFAULT_VALUES.TITLE,
+ description: DEFAULT_VALUES.DESCRIPTION,
+ icon: DEFAULT_VALUES.FAVICON,
+ url: url,
+ }
+ return defaultMetadata
+ }
+})
+
interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> {
onClose?: () => void
onSuccess?: () => void