dAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJw b z_^v8bbg` SAn{I*4bH$u(RZ6*x UhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=p C^ S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk( $?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU ^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvh CL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c 70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397* _cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111a H}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*I cmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU &68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-= A= yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v #ix45EVrcEhr>!NMhprl $InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~ &^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7< 4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}sc Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+ 9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2 `1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M =hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S( O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/trendfinder-ui/src/app/globals.css b/trendfinder-ui/src/app/globals.css new file mode 100644 index 0000000..6b717ad --- /dev/null +++ b/trendfinder-ui/src/app/globals.css @@ -0,0 +1,21 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/trendfinder-ui/src/app/layout.tsx b/trendfinder-ui/src/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/trendfinder-ui/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/trendfinder-ui/src/app/page.tsx b/trendfinder-ui/src/app/page.tsx new file mode 100644 index 0000000..158cdb9 --- /dev/null +++ b/trendfinder-ui/src/app/page.tsx @@ -0,0 +1,10 @@ +import { Button } from "@/components/button"; + +export default function Home() { + return ( + + + ) +} \ No newline at end of file diff --git a/trendfinder-ui/src/components/button.tsx b/trendfinder-ui/src/components/button.tsx new file mode 100644 index 0000000..f56dd4e --- /dev/null +++ b/trendfinder-ui/src/components/button.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributesHello Scraper UI
+ +, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef ( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } \ No newline at end of file diff --git a/trendfinder-ui/src/lib/utils.ts b/trendfinder-ui/src/lib/utils.ts new file mode 100644 index 0000000..33e174d --- /dev/null +++ b/trendfinder-ui/src/lib/utils.ts @@ -0,0 +1,44 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes , + VariantProps { + asChild?: boolean +} + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file diff --git a/trendfinder-ui/tailwind.config.ts b/trendfinder-ui/tailwind.config.ts new file mode 100644 index 0000000..109807b --- /dev/null +++ b/trendfinder-ui/tailwind.config.ts @@ -0,0 +1,18 @@ +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/trendfinder-ui/tsconfig.json b/trendfinder-ui/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/trendfinder-ui/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 1b9f5d873011659b641d00d93525fab1ef4a34bd Mon Sep 17 00:00:00 2001 From: Caleb Peffer <44934913+calebpeffer@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:45:38 -0800 Subject: [PATCH 2/5] Caleb: trigger chron jobs via UI --- package-lock.json | 56 ++++++++++++++++++++---- package.json | 9 ++-- src/api/index.ts | 20 +++++++++ src/services/scrapeSources.ts | 2 +- trendfinder-ui/src/app/api/cron/route.ts | 15 +++++++ trendfinder-ui/src/app/page.tsx | 55 +++++++++++++++++++++-- 6 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 src/api/index.ts create mode 100644 trendfinder-ui/src/app/api/cron/route.ts diff --git a/package-lock.json b/package-lock.json index bc42096..7b183e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,9 @@ "dependencies": { "@mendable/firecrawl-js": "^1.8.1", "@supabase/supabase-js": "^2.46.1", + "cors": "^2.8.5", "dotenv": "^14.3.2", - "express": "^4.18.2", + "express": "^4.21.2", "express-async-handler": "^1.2.0", "firecrawl": "^1.7.2", "node-cron": "^3.0.3", @@ -23,13 +24,14 @@ "ts-node-dev": "^2.0.0" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.10.2", "@types/node-cron": "^3.0.11", "nodemon": "^3.0.2", "pre-commit": "^1.2.2", "rimraf": "^5.0.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.3.2" } }, @@ -377,6 +379,16 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -1037,6 +1049,19 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1345,9 +1370,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -1369,7 +1394,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1384,6 +1409,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-async-handler": { @@ -2374,6 +2403,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -2561,9 +2599,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/peberminta": { diff --git a/package.json b/package.json index f3dd84d..5e2ab62 100644 --- a/package.json +++ b/package.json @@ -5,25 +5,28 @@ "scripts": { "start": "nodemon src/index.ts", "build": "tsc", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "api": "ts-node src/api/index.ts" }, "author": "", "license": "ISC", "devDependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.10.2", "@types/node-cron": "^3.0.11", "nodemon": "^3.0.2", "pre-commit": "^1.2.2", "rimraf": "^5.0.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.3.2" }, "dependencies": { "@mendable/firecrawl-js": "^1.8.1", "@supabase/supabase-js": "^2.46.1", + "cors": "^2.8.5", "dotenv": "^14.3.2", - "express": "^4.18.2", + "express": "^4.21.2", "express-async-handler": "^1.2.0", "firecrawl": "^1.7.2", "node-cron": "^3.0.3", diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..aa39fea --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,20 @@ +import express from 'express' +import cors from 'cors' +import { handleCron } from '../controllers/cron' + +const app = express() +app.use(cors()) +app.use(express.json()) + +app.post('/api/cron', async (req, res) => { + try { + await handleCron() + res.json({ success: true }) + } catch (error) { + res.status(500).json({ error: 'Failed to run cron' }) + } +}) + +app.listen(3001, () => { + console.log('API server running on port 3001') +}) \ No newline at end of file diff --git a/src/services/scrapeSources.ts b/src/services/scrapeSources.ts index 00f5d42..dd4cf97 100644 --- a/src/services/scrapeSources.ts +++ b/src/services/scrapeSources.ts @@ -30,7 +30,7 @@ export async function scrapeSources(sources: string[]) { // Configure these if you want to toggle behavior const useTwitter = true; - const useScrape = true; + const useScrape = false; for (const source of sources) { // --- 1) Handle x.com (Twitter) sources --- diff --git a/trendfinder-ui/src/app/api/cron/route.ts b/trendfinder-ui/src/app/api/cron/route.ts new file mode 100644 index 0000000..2f3d646 --- /dev/null +++ b/trendfinder-ui/src/app/api/cron/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server' + +export async function POST() { + try { + // Call the main project's handleCron function + const response = await fetch('http://localhost:3001/api/cron', { + method: 'POST', + }) + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + return NextResponse.json({ error: 'Failed to trigger cron' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/trendfinder-ui/src/app/page.tsx b/trendfinder-ui/src/app/page.tsx index 158cdb9..0290799 100644 --- a/trendfinder-ui/src/app/page.tsx +++ b/trendfinder-ui/src/app/page.tsx @@ -1,10 +1,57 @@ -import { Button } from "@/components/button"; +'use client' + +import { Button } from "@/components/button" +import { useState } from "react" export default function Home() { + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + const [message, setMessage] = useState ('') + + const handleClick = async () => { + try { + setStatus('loading') + const response = await fetch('/api/cron', { + method: 'POST', + }) + const data = await response.json() + + if (response.ok) { + setStatus('success') + setMessage('Cron job triggered successfully!') + } else { + throw new Error(data.error || 'Failed to trigger cron') + } + } catch (error) { + setStatus('error') + setMessage(error instanceof Error ? error.message : 'An error occurred') + } + } + return ( - - Hello Scraper UI
- ++ ) } \ No newline at end of file From 176dece9387661279ce0982ba76242e0892cbf70 Mon Sep 17 00:00:00 2001 From: Caleb Peffer <44934913+calebpeffer@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:20:17 -0800 Subject: [PATCH 3/5] Caleb: created prisma db and added settings to it --- package-lock.json | 84 ++++++++ package.json | 2 + prisma/dev.db | Bin 0 -> 36864 bytes .../migration.sql | 32 +++ .../migration.sql | 8 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 37 ++++ src/api/index.ts | 5 + src/index.ts | 26 ++- src/routes/config.ts | 80 ++++++++ trendfinder-ui/src/app/page.tsx | 58 +----- .../src/components/settings-form.tsx | 184 ++++++++++++++++++ trendfinder-ui/src/components/ui/card.tsx | 55 ++++++ trendfinder-ui/src/components/ui/input.tsx | 25 +++ 14 files changed, 537 insertions(+), 62 deletions(-) create mode 100644 prisma/dev.db create mode 100644 prisma/migrations/20250105025622_separate_x_accounts/migration.sql create mode 100644 prisma/migrations/20250105030156_api_keys_unique_service/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/routes/config.ts create mode 100644 trendfinder-ui/src/components/settings-form.tsx create mode 100644 trendfinder-ui/src/components/ui/card.tsx create mode 100644 trendfinder-ui/src/components/ui/input.tsx diff --git a/package-lock.json b/package-lock.json index 7b183e4..a6d6a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@mendable/firecrawl-js": "^1.8.1", + "@prisma/client": "^6.1.0", "@supabase/supabase-js": "^2.46.1", "cors": "^2.8.5", "dotenv": "^14.3.2", @@ -19,6 +20,7 @@ "node-cron": "^3.0.3", "openai": "^4.72.0", "playht": "^0.13.0", + "prisma": "^6.1.0", "resend": "^4.0.1-alpha.0", "together-ai": "^0.9.0", "ts-node-dev": "^2.0.0" @@ -159,6 +161,69 @@ "node": ">=14" } }, + "node_modules/@prisma/client": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.1.0.tgz", + "integrity": "sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.1.0.tgz", + "integrity": "sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.1.0.tgz", + "integrity": "sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.1.0", + "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", + "@prisma/fetch-engine": "6.1.0", + "@prisma/get-platform": "6.1.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959.tgz", + "integrity": "sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.1.0.tgz", + "integrity": "sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.1.0", + "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", + "@prisma/get-platform": "6.1.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.1.0.tgz", + "integrity": "sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.1.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2665,6 +2730,25 @@ "which": "1.2.x" } }, + "node_modules/prisma": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.1.0.tgz", + "integrity": "sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "6.1.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 5e2ab62..deac992 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@mendable/firecrawl-js": "^1.8.1", + "@prisma/client": "^6.1.0", "@supabase/supabase-js": "^2.46.1", "cors": "^2.8.5", "dotenv": "^14.3.2", @@ -32,6 +33,7 @@ "node-cron": "^3.0.3", "openai": "^4.72.0", "playht": "^0.13.0", + "prisma": "^6.1.0", "resend": "^4.0.1-alpha.0", "together-ai": "^0.9.0", "ts-node-dev": "^2.0.0" diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..ba34d260ab2db7078b7bb274c4e8293188969ff2 GIT binary patch literal 36864 zcmeI)%}(P+00(e85CSPQz2(3ndELW;c2oHycAQ&NaJMNCAS5dEVvX%Fxa4bNX9dzq zRfJmg0jjFc(90gW>an-JLR++ + + + {message && ( +TrendFinder UI
++ Sponsored by Firecrawl.dev 🔥 +
++ {message} +
+ )}^N}(0aijqmA2$RBC#_wo{!(y6L}J{zqjRw zRPfr}CJ6 -YS|7I!kH#fiVDYx`% z=Eud~7QcGu`}B7UuUG^E1Rwwb2tWV=5cppU9M7k+>N4NY_$`;7mOB0>JqxUWcAxtW zwNB_+9JR1sHA^*9s8`B+byFx;Hp~M-ydl3R>{P}H;;I-IC9d_YR4V)MA%DIQ61zdG zz1Amw5VpH#7kl$@CAHE=Tc#jhB#6RlT3|u+U6IvYGoPAOp|VpGD)p@`VYgb|E>#bO zP4iGF)oVND3M*&Ztkj+i4+=sO_5x9;nFs8M_^3FoNrQkK(P3g7=X5Cvsapy~VT1L# zR^B$phY1_z$EEsKO<1p2tE>tuO5LxOws+HO{V(TI+2v*ayguk*j1zBClYNV0@7S^7 zFx}iUHg-2B{JZYOHocDfMjIyZMKYCLS>ewg4FnEKj??b7!qIka(qI&QH-sKL!&;;n z58-&A?lhLS>g?|9T=wx}{&3LM;8}y+k+2KrS&z0HdgWzuqT^Q*SEJTHVMng@U(5{o zHV0+y(=hZ~N5N=2J5h=_ioP44(`~oPt&p8bq;bQwH9Fm8?Y{>-|C~={Wtsol8g$?4 zbp4=7tfqg|C86JL1=nK|lO)$OOyERQ$dxdd6E066SLO%jm^vpxuPII*chgkj`7J*< zrmjWU#BmWuQ%LML?u|7(Y2%DH71(GWv03X{h_c3ob=!>wtI#IS$>mzFRLSPQE}PFT z=ZFd& F0n`A-JiJ(VNVDUfB*y_ z009U<00Izz00bZafwxkiFC^0S{!;Rg Z(nbVFya6 6@uer?Yd(%O51OgC%00bZa0SG_<0uX=z1RyYl0uK^N?)_wPoiwPuOFfTv*?<0@ z{_xB28Q=e(!cziWh5!U0009U<00Izz00bZafvFR~{r}VrIXVmh2tWV=5P$##AOHaf oKmY<$C=mVrKYssj3I`rth5!U0009U<00Izz00bZafvFPs2bykvrT_o{ literal 0 HcmV?d00001 diff --git a/prisma/migrations/20250105025622_separate_x_accounts/migration.sql b/prisma/migrations/20250105025622_separate_x_accounts/migration.sql new file mode 100644 index 0000000..cf19d1c --- /dev/null +++ b/prisma/migrations/20250105025622_separate_x_accounts/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "Settings" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "cronInterval" INTEGER NOT NULL DEFAULT 15, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "XAccount" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "ApiKeys" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "service" TEXT NOT NULL, + "key" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "RunHistory" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "status" TEXT NOT NULL, + "message" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/prisma/migrations/20250105030156_api_keys_unique_service/migration.sql b/prisma/migrations/20250105030156_api_keys_unique_service/migration.sql new file mode 100644 index 0000000..bb79591 --- /dev/null +++ b/prisma/migrations/20250105030156_api_keys_unique_service/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[service]` on the table `ApiKeys` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeys_service_key" ON "ApiKeys"("service"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e1640d1 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..973f939 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,37 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model Settings { + id Int @id @default(autoincrement()) + cronInterval Int @default(15) // in minutes + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model XAccount { + id Int @id @default(autoincrement()) + username String // Store each X account as a separate record + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ApiKeys { + id Int @id @default(autoincrement()) + service String @unique + key String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model RunHistory { + id Int @id @default(autoincrement()) + status String // "success" | "error" + message String? + createdAt DateTime @default(now()) +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index aa39fea..95b314a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,11 +1,16 @@ import express from 'express' import cors from 'cors' import { handleCron } from '../controllers/cron' +import configRouter from '../routes/config' const app = express() app.use(cors()) app.use(express.json()) +// Mount config routes +app.use('/api', configRouter) + +// Original cron endpoint app.post('/api/cron', async (req, res) => { try { await handleCron() diff --git a/src/index.ts b/src/index.ts index 2a12fbc..581f32f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,16 @@ -import { handleCron } from "./controllers/cron" -import cron from 'node-cron'; -import dotenv from 'dotenv'; +import express from 'express'; +import cors from 'cors'; +import configRouter from './routes/config'; -dotenv.config(); +const app = express(); -async function main() { - console.log(`Starting process to generate draft...`); - await handleCron(); -} -main(); +app.use(cors()); +app.use(express.json()); +// Mount the config routes +app.use('/api', configRouter); -// If you want to run the cron job manually, uncomment the following line: -//cron.schedule(`0 17 * * *`, async () => { -// console.log(`Starting process to generate draft...`); -// await handleCron(); -//}); \ No newline at end of file +const PORT = process.env.PORT || 3001; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); \ No newline at end of file diff --git a/src/routes/config.ts b/src/routes/config.ts new file mode 100644 index 0000000..4e8be62 --- /dev/null +++ b/src/routes/config.ts @@ -0,0 +1,80 @@ +import express from 'express'; +import { PrismaClient } from '@prisma/client'; + +const router = express.Router(); +const prisma = new PrismaClient(); + +// Get all settings +router.get('/settings', async (req, res) => { + try { + const settings = await prisma.settings.findFirst(); + const xAccounts = await prisma.xAccount.findMany(); + const apiKeys = await prisma.apiKeys.findMany(); + + res.json({ + settings, + xAccounts, + apiKeys + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch settings' }); + } +}); + +// Update settings +router.post('/settings', async (req, res) => { + try { + const { cronInterval } = req.body; + + const settings = await prisma.settings.upsert({ + where: { id: 1 }, + update: { cronInterval }, + create: { cronInterval } + }); + + res.json(settings); + } catch (error) { + res.status(500).json({ error: 'Failed to update settings' }); + } +}); + +// Manage X accounts +router.post('/x-accounts', async (req, res) => { + try { + const { username } = req.body; + const account = await prisma.xAccount.create({ + data: { username } + }); + res.json(account); + } catch (error) { + res.status(500).json({ error: 'Failed to add X account' }); + } +}); + +router.delete('/x-accounts/:id', async (req, res) => { + try { + await prisma.xAccount.delete({ + where: { id: parseInt(req.params.id) } + }); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete X account' }); + } +}); + +// Manage API keys +router.post('/api-keys', async (req, res) => { + try { + const { service, key }: { service: string; key: string } = req.body; + const apiKey = await prisma.apiKeys.upsert({ + where: { id: (await prisma.apiKeys.findFirst({ where: { service } }))?.id ?? 0 }, + update: { key, service }, + create: { service, key } + }); + res.json(apiKey); + } catch (error) { + res.status(500).json({ error: 'Failed to update API key' }); + } +}); + +export default router; \ No newline at end of file diff --git a/trendfinder-ui/src/app/page.tsx b/trendfinder-ui/src/app/page.tsx index 0290799..d9918f7 100644 --- a/trendfinder-ui/src/app/page.tsx +++ b/trendfinder-ui/src/app/page.tsx @@ -1,57 +1,19 @@ 'use client' -import { Button } from "@/components/button" -import { useState } from "react" +import { SettingsForm } from "@/components/settings-form" export default function Home() { - const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') - const [message, setMessage] = useState ('') - - const handleClick = async () => { - try { - setStatus('loading') - const response = await fetch('/api/cron', { - method: 'POST', - }) - const data = await response.json() - - if (response.ok) { - setStatus('success') - setMessage('Cron job triggered successfully!') - } else { - throw new Error(data.error || 'Failed to trigger cron') - } - } catch (error) { - setStatus('error') - setMessage(error instanceof Error ? error.message : 'An error occurred') - } - } - return ( - - -TrendFinder UI
-- Sponsored by Firecrawl.dev 🔥 -
++ ) } \ No newline at end of file diff --git a/trendfinder-ui/src/components/settings-form.tsx b/trendfinder-ui/src/components/settings-form.tsx new file mode 100644 index 0000000..3701a0c --- /dev/null +++ b/trendfinder-ui/src/components/settings-form.tsx @@ -0,0 +1,184 @@ +'use client' + +import { useState, useEffect } from "react" +import { Button } from "./button" +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" +import { Input } from "./ui/input" + + +interface XAccount { + id: number + username: string +} + +interface Settings { + cronInterval: number +} + +interface ApiKey { + service: string + key: string +} + +export function SettingsForm() { + const [settings, setSettings] = useState+- - - - {message && ( -++TrendFinder UI
++ Sponsored by Firecrawl.dev 🔥 +
+- {message} -
- )}({ cronInterval: 15 }) + const [xAccounts, setXAccounts] = useState ([]) + const [newAccount, setNewAccount] = useState('') + const [apiKeys, setApiKeys] = useState ([]) + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + + // Fetch initial settings + useEffect(() => { + fetchSettings() + }, []) + + const fetchSettings = async () => { + try { + const response = await fetch('http://localhost:3001/api/settings') + const data = await response.json() + setSettings(data.settings) + setXAccounts(data.xAccounts) + setApiKeys(data.apiKeys) + } catch (error) { + console.error('Failed to fetch settings:', error) + } + } + + const handleAddAccount = async () => { + try { + const response = await fetch('http://localhost:3001/api/x-accounts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: newAccount }) + }) + const data = await response.json() + setXAccounts([...xAccounts, data]) + setNewAccount('') + } catch (error) { + console.error('Failed to add account:', error) + } + } + + const handleRemoveAccount = async (id: number) => { + try { + await fetch(`http://localhost:3001/api/x-accounts/${id}`, { + method: 'DELETE' + }) + setXAccounts(xAccounts.filter(account => account.id !== id)) + } catch (error) { + console.error('Failed to remove account:', error) + } + } + + const handleUpdateApiKey = async (service: string, key: string) => { + try { + const response = await fetch('http://localhost:3001/api/api-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ service, key }) + }) + const data = await response.json() + setApiKeys(apiKeys.map(k => k.service === service ? data : k)) + } catch (error) { + console.error('Failed to update API key:', error) + } + } + + const handleUpdateSettings = async () => { + try { + setStatus('loading') + const response = await fetch('http://localhost:3001/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings) + }) + if (response.ok) { + setStatus('success') + } else { + throw new Error('Failed to update settings') + } + } catch (error) { + setStatus('error') + } + } + + return ( + + {/* X Accounts Section */} ++ ) +} \ No newline at end of file diff --git a/trendfinder-ui/src/components/ui/card.tsx b/trendfinder-ui/src/components/ui/card.tsx new file mode 100644 index 0000000..caae08f --- /dev/null +++ b/trendfinder-ui/src/components/ui/card.tsx @@ -0,0 +1,55 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes+ + + {/* API Keys Section */} ++ +X Accounts to Monitor ++ ++ setNewAccount(e.target.value)} + placeholder="@username" + /> + +++ {xAccounts.map(account => ( +++ {account.username} + ++ ))} ++ + + {/* Cron Settings Section */} ++ +API Keys ++ {['together', 'x', 'firecrawl', 'slack'].map(service => ( + ++ + k.service === service)?.key || ''} + onChange={(e) => handleUpdateApiKey(service, e.target.value)} + placeholder={`Enter ${service} API key`} + /> ++ ))} ++ ++ +Check Interval ++ ++ setSettings({ ...settings, cronInterval: Number(e.target.value) })} + /> + minutes ++ ++>(({ className, ...props }, ref) => ( + +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardTitle.displayName = "CardTitle" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardContent.displayName = "CardContent" + +export { Card, CardHeader, CardTitle, CardContent } \ No newline at end of file diff --git a/trendfinder-ui/src/components/ui/input.tsx b/trendfinder-ui/src/components/ui/input.tsx new file mode 100644 index 0000000..28a4c5d --- /dev/null +++ b/trendfinder-ui/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef ( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } \ No newline at end of file From 743406a3f96c441fee9ad7600cbc4c36e0f4db98 Mon Sep 17 00:00:00 2001 From: Caleb Peffer <44934913+calebpeffer@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:27:00 -0800 Subject: [PATCH 4/5] Caleb: in prog --- src/lib/prisma.ts | 4 ++ src/services/generateDraft.ts | 14 +++- src/services/getCronSources.ts | 66 ++++--------------- src/services/scrapeSources.ts | 10 +++ src/services/sendDraft.ts | 24 +++---- trendfinder-ui/prisma/schema.prisma | 1 + .../src/components/settings-form.tsx | 50 ++++++++++++++ 7 files changed, 103 insertions(+), 66 deletions(-) create mode 100644 src/lib/prisma.ts create mode 100644 trendfinder-ui/prisma/schema.prisma diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..ff36d9c --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,4 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() +export default prisma \ No newline at end of file diff --git a/src/services/generateDraft.ts b/src/services/generateDraft.ts index 9e5175a..2e8b276 100644 --- a/src/services/generateDraft.ts +++ b/src/services/generateDraft.ts @@ -2,6 +2,7 @@ import dotenv from 'dotenv'; import Together from 'together-ai'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import prisma from '../lib/prisma' dotenv.config(); @@ -12,8 +13,17 @@ export async function generateDraft(rawStories: string) { console.log(`Generating a post draft with raw stories (${rawStories.length} characters)...`) try { - // Initialize Together client - const together = new Together(); + // Get Together AI key from database + const togetherKey = await prisma.apiKeys.findFirst({ + where: { service: 'together' } + }) + + if (!togetherKey?.key) { + throw new Error('Together AI key not configured') + } + + // Initialize Together with key from database + const together = new Together({ apiKey: togetherKey.key }); // Define the schema for our response const DraftPostSchema = z.object({ diff --git a/src/services/getCronSources.ts b/src/services/getCronSources.ts index 613cf82..31544be 100644 --- a/src/services/getCronSources.ts +++ b/src/services/getCronSources.ts @@ -1,19 +1,19 @@ - -import dotenv from 'dotenv'; - -dotenv.config(); +import prisma from '../lib/prisma' export async function getCronSources() { try { console.log("Fetching sources..."); - // Check for required API keys - const hasXApiKey = !!process.env.X_API_BEARER_TOKEN; - const hasFirecrawlKey = !!process.env.FIRECRAWL_API_KEY; + // Get API keys from database + const apiKeys = await prisma.apiKeys.findMany() + const hasXApiKey = apiKeys.some(key => key.service === 'x') + const hasFirecrawlKey = apiKeys.some(key => key.service === 'firecrawl') - // Filter sources based on available API keys + // Get X accounts from database + const xAccounts = await prisma.xAccount.findMany() + const sources = [ - // High priority sources (Only 1 x account due to free plan rate limits) + // Firecrawl sources ...(hasFirecrawlKey ? [ { identifier: 'https://www.firecrawl.dev/blog' }, { identifier: 'https://openai.com/news/' }, @@ -23,51 +23,11 @@ export async function getCronSources() { { identifier: 'https://simonwillison.net/' }, { identifier: 'https://buttondown.com/ainews/archive/' }, ] : []), - ...(hasXApiKey ? [ - { identifier: 'https://x.com/skirano' }, - // Official AI Companies - // { identifier: 'https://x.com/OpenAIDevs' }, - // { identifier: 'https://x.com/xai' }, - // { identifier: 'https://x.com/alexalbert__' }, - // { identifier: 'https://x.com/leeerob' }, - // { identifier: 'https://x.com/v0' }, - // { identifier: 'https://x.com/aisdk' }, - // { identifier: 'https://x.com/firecrawl_dev' }, - // { identifier: 'https://x.com/AIatMeta' }, - // { identifier: 'https://x.com/googleaidevs' }, - - // Additional AI Companies - // { identifier: 'https://x.com/MistralAI' }, - // { identifier: 'https://x.com/Cohere' }, - // AI Researchers & Thought Leaders - // { identifier: 'https://x.com/karpathy' }, - // { identifier: 'https://x.com/ylecun' }, - // { identifier: 'https://x.com/sama' }, - // { identifier: 'https://x.com/EMostaque' }, - // { identifier: 'https://x.com/DrJimFan' }, - // { identifier: 'https://x.com/nickscamara_' }, - // { identifier: 'https://x.com/CalebPeffer' }, - // { identifier: 'https://x.com/akshay_pachaar' }, - // { identifier: 'https://x.com/ericciarla' }, - // { identifier: 'https://x.com/amasad' }, - // { identifier: 'https://x.com/nutlope' }, - // { identifier: 'https://x.com/rauchg' }, - - // AI Tools & Platforms - // { identifier: 'https://x.com/vercel' }, - // { identifier: 'https://x.com/LangChainAI' }, - // { identifier: 'https://x.com/llama_index' }, - // { identifier: 'https://x.com/pinecone' }, - // { identifier: 'https://x.com/modal_labs' }, - - // AI News & Blogs - // { identifier: 'https://x.com/huggingface' }, - // { identifier: 'https://x.com/weights_biases' }, - // { identifier: 'https://x.com/replicate' }, - ] : []), - - + // X/Twitter sources + ...(hasXApiKey ? xAccounts.map(account => ({ + identifier: `https://x.com/${account.username}` + })) : []), ]; return sources.map(source => source.identifier); diff --git a/src/services/scrapeSources.ts b/src/services/scrapeSources.ts index dd4cf97..b584725 100644 --- a/src/services/scrapeSources.ts +++ b/src/services/scrapeSources.ts @@ -3,9 +3,19 @@ import dotenv from 'dotenv'; // Removed Together import import { z } from 'zod'; // Removed zodToJsonSchema import since we no longer enforce JSON output via Together +import prisma from '../lib/prisma' dotenv.config(); +// Get API keys at runtime +async function getApiKeys() { + const keys = await prisma.apiKeys.findMany() + return { + firecrawlKey: keys.find(k => k.service === 'firecrawl')?.key, + xApiKey: keys.find(k => k.service === 'x')?.key + } +} + // Initialize Firecrawl const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY }); diff --git a/src/services/sendDraft.ts b/src/services/sendDraft.ts index dcb89c6..e946177 100644 --- a/src/services/sendDraft.ts +++ b/src/services/sendDraft.ts @@ -1,19 +1,21 @@ import axios from 'axios'; -import dotenv from 'dotenv'; -dotenv.config(); +import prisma from '../lib/prisma' export async function sendDraft(draft_post: string) { try { + // Get Slack webhook URL from database + const slackKey = await prisma.apiKeys.findFirst({ + where: { service: 'slack' } + }) + + if (!slackKey?.key) { + throw new Error('Slack webhook URL not configured') + } + const response = await axios.post( - process.env.SLACK_WEBHOOK_URL || '', - { - text: draft_post, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - } + slackKey.key, + { text: draft_post }, + { headers: { 'Content-Type': 'application/json' } } ); return `Success sending draft to webhook at ${new Date().toISOString()}`; diff --git a/trendfinder-ui/prisma/schema.prisma b/trendfinder-ui/prisma/schema.prisma new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/trendfinder-ui/prisma/schema.prisma @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/trendfinder-ui/src/components/settings-form.tsx b/trendfinder-ui/src/components/settings-form.tsx index 3701a0c..9dbc293 100644 --- a/trendfinder-ui/src/components/settings-form.tsx +++ b/trendfinder-ui/src/components/settings-form.tsx @@ -26,6 +26,8 @@ export function SettingsForm() { const [newAccount, setNewAccount] = useState('') const [apiKeys, setApiKeys] = useState ([]) const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + const [cronStatus, setCronStatus] = useState<'idle' | 'running' | 'success' | 'error'>('idle') + const [cronMessage, setCronMessage] = useState('') // Fetch initial settings useEffect(() => { @@ -102,6 +104,28 @@ export function SettingsForm() { } } + const handleTestCron = async () => { + try { + setCronStatus('running') + setCronMessage('') + + const response = await fetch('http://localhost:3001/api/cron', { + method: 'POST' + }) + + if (!response.ok) { + throw new Error('Failed to run cron job') + } + + const data = await response.json() + setCronStatus('success') + setCronMessage('Cron job completed successfully!') + } catch (error) { + setCronStatus('error') + setCronMessage(error instanceof Error ? error.message : 'Failed to run cron job') + } + } + return ( {/* X Accounts Section */} @@ -179,6 +203,32 @@ export function SettingsForm() { + + {/* Test Cron Section */} +) } \ No newline at end of file From 6cc5a867fcf4a070a3e2be7d6980a4e0f554dd5b Mon Sep 17 00:00:00 2001 From: Eric Ciarla+ + +Test Run ++ ++ + + {cronMessage && ( +++ {cronMessage} +
+ )} +Date: Sun, 5 Jan 2025 20:21:34 -0500 Subject: [PATCH 5/5] Minor changes! --- trendfinder-ui/src/app/page.tsx | 2 +- trendfinder-ui/src/components/settings-form.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/trendfinder-ui/src/app/page.tsx b/trendfinder-ui/src/app/page.tsx index d9918f7..f72697d 100644 --- a/trendfinder-ui/src/app/page.tsx +++ b/trendfinder-ui/src/app/page.tsx @@ -9,7 +9,7 @@ export default function Home() { TrendFinder UI
- Sponsored by Firecrawl.dev 🔥 + Sponsored by Firecrawl.dev 🔥
diff --git a/trendfinder-ui/src/components/settings-form.tsx b/trendfinder-ui/src/components/settings-form.tsx index 9dbc293..0541d05 100644 --- a/trendfinder-ui/src/components/settings-form.tsx +++ b/trendfinder-ui/src/components/settings-form.tsx @@ -131,14 +131,14 @@ export function SettingsForm() { {/* X Accounts Section */} - X Accounts to Monitor +X Accounts / Webpages to Monitor setNewAccount(e.target.value)} - placeholder="@username" + placeholder="@username or https://example.com/blog" />