From 8f267f31881e40167d880ea5ac98d0e72004b114 Mon Sep 17 00:00:00 2001 From: ElektrikSpark Date: Fri, 10 May 2024 02:44:27 +0000 Subject: [PATCH] basic demo working --- apps/api/src/api/api_v1/api.py | 3 +- apps/api/src/api/api_v1/routers/health.py | 4 +- apps/api/src/api/api_v1/routers/upload.py | 4 +- apps/api/src/config.py | 2 +- apps/api/src/main.py | 31 +++-- apps/nextjs/src/app/(app)/account/page.tsx | 10 +- apps/nextjs/src/app/(app)/dashboard/page.tsx | 16 +-- .../src/app/(app)/reports/[reportId]/page.tsx | 38 ++++++ apps/nextjs/src/app/(app)/reports/page.tsx | 17 ++- apps/nextjs/src/app/layout.tsx | 6 + .../src/components/reports/report-form.tsx | 8 +- .../src/components/reports/report-list.tsx | 20 ++-- .../src/components/shared/back-button.tsx | 33 ++++++ .../src/components/upload/file-upload.tsx | 109 ++++++++++++++++++ apps/nextjs/src/env.ts | 1 + packages/api/package.json | 4 + packages/api/src/index.ts | 3 +- .../api/src/lib/api/client/schemas.gen.ts | 4 +- .../api/src/lib/api/client/services.gen.ts | 14 +-- packages/api/src/lib/api/client/types.gen.ts | 16 +-- packages/api/src/router/report.ts | 2 +- 21 files changed, 286 insertions(+), 59 deletions(-) create mode 100644 apps/nextjs/src/app/(app)/reports/[reportId]/page.tsx create mode 100644 apps/nextjs/src/components/shared/back-button.tsx create mode 100644 apps/nextjs/src/components/upload/file-upload.tsx diff --git a/apps/api/src/api/api_v1/api.py b/apps/api/src/api/api_v1/api.py index 3605726..4a1e472 100644 --- a/apps/api/src/api/api_v1/api.py +++ b/apps/api/src/api/api_v1/api.py @@ -2,8 +2,9 @@ from src.api.api_v1.routers import health, upload - api_router = APIRouter() + + api_router.include_router( health.router, prefix="/health", diff --git a/apps/api/src/api/api_v1/routers/health.py b/apps/api/src/api/api_v1/routers/health.py index 857393b..765c8e4 100644 --- a/apps/api/src/api/api_v1/routers/health.py +++ b/apps/api/src/api/api_v1/routers/health.py @@ -6,8 +6,8 @@ router = APIRouter() -@router.get("/health", status_code=200) -async def health_check(user: str = Depends(verify_token)): +@router.get("", status_code=200) +async def check(user: str = Depends(verify_token)): """Secured health check endpoint.""" if not user: raise HTTPException(status_code=401, detail="Unauthorized") diff --git a/apps/api/src/api/api_v1/routers/upload.py b/apps/api/src/api/api_v1/routers/upload.py index 87c7a3c..ee05bac 100644 --- a/apps/api/src/api/api_v1/routers/upload.py +++ b/apps/api/src/api/api_v1/routers/upload.py @@ -17,8 +17,8 @@ router = APIRouter() -@router.post("/upload", status_code=200) -async def upload_zip(file: UploadFile = File(...), user: str = Depends(verify_token)): +@router.post("", status_code=200) +async def zip(file: UploadFile = File(...), user: str = Depends(verify_token)): """Secure endpoint to upload and process zip files, ensuring valid JWTs.""" try: with tempfile.TemporaryDirectory() as temp_dir: diff --git a/apps/api/src/config.py b/apps/api/src/config.py index 8c27740..2c89023 100644 --- a/apps/api/src/config.py +++ b/apps/api/src/config.py @@ -20,7 +20,7 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") openapi_url: str = "/openapi.json" - API_VERSION: str = "/api/v1" + API_VERSION: str = "/v1" ROOT: str = ROOT_PATH diff --git a/apps/api/src/main.py b/apps/api/src/main.py index 9f918d9..49591f2 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -1,3 +1,6 @@ +import logging +import os + from fastapi import APIRouter, FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.routing import APIRoute @@ -35,13 +38,27 @@ def get_application(): openapi_url=settings.openapi_url, ) - _app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) + origins = ["https://app.getwellchart.com"] + environment = os.getenv("VERCEL_ENV", "development") + + if environment in ["development"]: + logger = logging.getLogger("uvicorn") + logger.warning("Running in development mode - allowing CORS for all origins") + _app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + else: + _app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) _app.include_router(api_router, prefix=settings.API_VERSION) _app.include_router(info_router, tags=[""]) diff --git a/apps/nextjs/src/app/(app)/account/page.tsx b/apps/nextjs/src/app/(app)/account/page.tsx index 48c57c7..3f7501d 100644 --- a/apps/nextjs/src/app/(app)/account/page.tsx +++ b/apps/nextjs/src/app/(app)/account/page.tsx @@ -1,3 +1,4 @@ +import { SignOutButton } from "~/components/auth/sign-out-button"; import { createClient } from "~/utils/supabase/server"; import UserSettings from "./user-settings"; @@ -11,9 +12,12 @@ export default async function Account() { return (
-

Account

-
- +
+

Account

+
+ +
+
); diff --git a/apps/nextjs/src/app/(app)/dashboard/page.tsx b/apps/nextjs/src/app/(app)/dashboard/page.tsx index 87516e8..1e7d60c 100644 --- a/apps/nextjs/src/app/(app)/dashboard/page.tsx +++ b/apps/nextjs/src/app/(app)/dashboard/page.tsx @@ -1,22 +1,18 @@ -import { SignOutButton } from "~/components/auth/sign-out-button"; -import { api } from "~/trpc/server"; +import { FileUpload } from "~/components/upload/file-upload"; import { createClient } from "~/utils/supabase/server"; export default async function Home() { const supabase = createClient(); const { data } = await supabase.auth.getSession(); - const test = await api.report.test({ - token: data?.session?.access_token ?? "", - }); - - console.log(test); + const token = data?.session?.access_token ?? ""; return (
-

Profile

-

{test?.message}

- +
+

Upload a patient's chart

+ +
); } diff --git a/apps/nextjs/src/app/(app)/reports/[reportId]/page.tsx b/apps/nextjs/src/app/(app)/reports/[reportId]/page.tsx new file mode 100644 index 0000000..1a04bb0 --- /dev/null +++ b/apps/nextjs/src/app/(app)/reports/[reportId]/page.tsx @@ -0,0 +1,38 @@ +import { Suspense } from "react"; +import { notFound } from "next/navigation"; + +import Loading from "~/app/loading"; +import ReportModal from "~/components/reports/report-modal"; +import { BackButton } from "~/components/shared/back-button"; +import { api } from "~/trpc/server"; + +export default async function Report({ + params, +}: { + params: { reportId: string }; +}) { + const { report } = await api.report.byId({ id: Number(params.reportId) }); + + if (!report) notFound(); + return ( +
+ }> +
+ + +
+
+

{report.title}

+
+ +
+
+
+              {report.content}
+            
+
+
+
+
+ ); +} diff --git a/apps/nextjs/src/app/(app)/reports/page.tsx b/apps/nextjs/src/app/(app)/reports/page.tsx index 906b8af..1e4fbc7 100644 --- a/apps/nextjs/src/app/(app)/reports/page.tsx +++ b/apps/nextjs/src/app/(app)/reports/page.tsx @@ -1,9 +1,12 @@ +import { Suspense } from "react"; + +import Loading from "~/app/loading"; import ReportList from "~/components/reports/report-list"; import NewReportModal from "~/components/reports/report-modal"; import { api } from "~/trpc/server"; -export default async function Reports() { - const reports = await api.report.byUser(); +export default function Reports() { + const reports = api.report.byUser(); return (
@@ -11,7 +14,15 @@ export default async function Reports() {

Reports

- + + + + } + > + +
); } diff --git a/apps/nextjs/src/app/layout.tsx b/apps/nextjs/src/app/layout.tsx index 1714bd2..f1c6ba9 100644 --- a/apps/nextjs/src/app/layout.tsx +++ b/apps/nextjs/src/app/layout.tsx @@ -10,8 +10,14 @@ import { TRPCReactProvider } from "~/trpc/react"; import "~/app/globals.css"; +import { OpenAPI } from "@wellchart/api/client"; + import { env } from "~/env"; +if (process.env.NODE_ENV === "production") { + OpenAPI.BASE = env.FASTAPI_URL; +} + export const metadata: Metadata = { metadataBase: new URL( env.VERCEL_ENV === "production" diff --git a/apps/nextjs/src/components/reports/report-form.tsx b/apps/nextjs/src/components/reports/report-form.tsx index e90aa1d..54baef6 100644 --- a/apps/nextjs/src/components/reports/report-form.tsx +++ b/apps/nextjs/src/components/reports/report-form.tsx @@ -49,7 +49,13 @@ const ReportForm = ({ } await utils.report.byUser.invalidate(); - router.refresh(); + + if (action === "delete") { + router.push("/reports"); + } else if (action === "update") { + router.refresh(); + } + if (closeModal) closeModal(); toast.success(`Report ${action}d!`); }; diff --git a/apps/nextjs/src/components/reports/report-list.tsx b/apps/nextjs/src/components/reports/report-list.tsx index b9f928e..304bc31 100644 --- a/apps/nextjs/src/components/reports/report-list.tsx +++ b/apps/nextjs/src/components/reports/report-list.tsx @@ -1,21 +1,21 @@ "use client"; +import { use } from "react"; +import Link from "next/link"; + import type { RouterOutputs } from "@wellchart/api"; import type { Report } from "@wellchart/db/schema"; +import { Button } from "@wellchart/ui/button"; import { api } from "~/trpc/react"; import ReportModal from "./report-modal"; -type UserReportsOutput = RouterOutputs["report"]["byUser"]; - -export default function ReportList({ - reports, -}: { - reports: UserReportsOutput; +export default function ReportList(props: { + reports: Promise; }) { + const initialData = use(props.reports); const { data: r } = api.report.byUser.useQuery(undefined, { - initialData: reports, - refetchOnMount: false, + initialData, }); if (r.reports.length === 0) { @@ -37,7 +37,9 @@ const Report = ({ report }: { report: Report }) => {
{report.title}
- + ); }; diff --git a/apps/nextjs/src/components/shared/back-button.tsx b/apps/nextjs/src/components/shared/back-button.tsx new file mode 100644 index 0000000..66a4b98 --- /dev/null +++ b/apps/nextjs/src/components/shared/back-button.tsx @@ -0,0 +1,33 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChevronLeftIcon } from "lucide-react"; + +import { Button } from "@wellchart/ui/button"; + +export function useBackPath(currentResource: string) { + const pathname = usePathname(); + const segmentCount = pathname.slice(1).split("/"); + const backPath = + segmentCount.length > 2 + ? pathname.slice(0, pathname.indexOf(currentResource) - 1) + : pathname.slice(0, pathname.indexOf(segmentCount[1])); + return backPath; +} + +export function BackButton({ + currentResource, +}: { + /* must be in kebab-case */ + currentResource: string; +}) { + const backPath = useBackPath(currentResource); + return ( + + ); +} diff --git a/apps/nextjs/src/components/upload/file-upload.tsx b/apps/nextjs/src/components/upload/file-upload.tsx new file mode 100644 index 0000000..8d6b1e4 --- /dev/null +++ b/apps/nextjs/src/components/upload/file-upload.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useRef, useState } from "react"; +import { useRouter } from "next/navigation"; + +import { Button } from "@wellchart/ui/button"; +import { toast } from "@wellchart/ui/sonner"; + +import { api } from "~/trpc/react"; + +const FileUpload = (props: { token: string }) => { + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const fileInputRef = useRef(null); + const router = useRouter(); + + const utils = api.useUtils(); + const createReport = api.report.create.useMutation({ + onSuccess: async () => { + await utils.report.invalidate(); + setUploading(false); + router.push("/reports"); + }, + onError: (err) => { + toast.error( + err.data?.code === "UNAUTHORIZED" + ? "You must be logged in" + : "Failed to create report", + ); + }, + }); + + const getFormattedDate = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = (today.getMonth() + 1).toString().padStart(2, "0"); + const day = today.getDate().toString().padStart(2, "0"); + return `${month}/${day}/${year}`; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!file) { + alert("Please select a file to upload."); + return; + } + + setUploading(true); + + // Create the FormData object to upload the file + const formData = new FormData(); + formData.append("file", file); + + try { + const response = await fetch("http://127.0.0.1:8000/v1/upload", { + method: "POST", + headers: { + Authorization: `Bearer ${props.token}`, + }, + body: formData, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.json(); + console.log("Upload successful:", responseData.message || responseData); + + const currentDate = getFormattedDate(); + createReport.mutate({ + title: `Mary Smith - ${currentDate}`, + content: responseData.prechart, + }); + } catch (error) { + console.error("Upload Error:", error); + alert("Upload failed."); + } + + // Reset the file state and clear the input + setFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( +
+ { + const files = e.target.files; + if (files) { + setFile(files[0] as File); + } + }} + accept=".zip" + /> + +
+ ); +}; + +export { FileUpload }; diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index 5d3ae07..6f9ec14 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -16,6 +16,7 @@ export const env = createEnv({ */ server: { POSTGRES_URL: z.string().min(1), + FASTAPI_URL: z.string().min(1), }, /** * Specify your client-side environment variables schema here. diff --git a/packages/api/package.json b/packages/api/package.json index bc9c06e..262c4a4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -7,6 +7,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./src/index.ts" + }, + "./client": { + "types": "./dist/client.d.ts", + "default": "./src/lib/api/client/index.ts" } }, "license": "MIT", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 7321e1b..dd975b0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -6,8 +6,7 @@ import { appRouter } from "./root"; import { createCallerFactory, createTRPCContext } from "./trpc"; if (process.env.NODE_ENV === "production") { - // FIXME: FastAPI deployment goes here - OpenAPI.BASE = "https://change-this-urlllll.vercel.app"; + OpenAPI.BASE = process.env.FASTAPI_URL ?? "https://api.getwellchart.com"; } /** diff --git a/packages/api/src/lib/api/client/schemas.gen.ts b/packages/api/src/lib/api/client/schemas.gen.ts index 896cf90..2afba43 100644 --- a/packages/api/src/lib/api/client/schemas.gen.ts +++ b/packages/api/src/lib/api/client/schemas.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export const $Body_upload_upload_zip = { +export const $Body_upload_zip = { properties: { file: { type: 'string', @@ -10,7 +10,7 @@ export const $Body_upload_upload_zip = { }, type: 'object', required: ['file'], - title: 'Body_upload-upload_zip' + title: 'Body_upload-zip' } as const; export const $HTTPValidationError = { diff --git a/packages/api/src/lib/api/client/services.gen.ts b/packages/api/src/lib/api/client/services.gen.ts index e436647..150fb1c 100644 --- a/packages/api/src/lib/api/client/services.gen.ts +++ b/packages/api/src/lib/api/client/services.gen.ts @@ -3,19 +3,19 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { HealthHealthCheckResponse, UploadUploadZipData, UploadUploadZipResponse } from './types.gen'; +import type { HealthCheckResponse, UploadZipData, UploadZipResponse } from './types.gen'; export class HealthService { /** - * Health Check + * Check * Secured health check endpoint. * @returns unknown Successful Response * @throws ApiError */ - public static healthHealthCheck(): CancelablePromise { + public static healthCheck(): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/api/v1/health/health', + url: '/v1/health', errors: { 404: 'Not found' } @@ -26,17 +26,17 @@ export class HealthService { export class UploadService { /** - * Upload Zip + * Zip * Secure endpoint to upload and process zip files, ensuring valid JWTs. * @param data The data for the request. * @param data.formData * @returns unknown Successful Response * @throws ApiError */ - public static uploadUploadZip(data: UploadUploadZipData): CancelablePromise { + public static uploadZip(data: UploadZipData): CancelablePromise { return __request(OpenAPI, { method: 'POST', - url: '/api/v1/upload/upload', + url: '/v1/upload', formData: data.formData, mediaType: 'multipart/form-data', errors: { diff --git a/packages/api/src/lib/api/client/types.gen.ts b/packages/api/src/lib/api/client/types.gen.ts index b289e8e..2b1513a 100644 --- a/packages/api/src/lib/api/client/types.gen.ts +++ b/packages/api/src/lib/api/client/types.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type Body_upload_upload_zip = { +export type Body_upload_zip = { file: (Blob | File); }; @@ -14,16 +14,16 @@ export type ValidationError = { type: string; }; -export type HealthHealthCheckResponse = unknown; +export type HealthCheckResponse = unknown; -export type UploadUploadZipData = { - formData: Body_upload_upload_zip; +export type UploadZipData = { + formData: Body_upload_zip; }; -export type UploadUploadZipResponse = unknown; +export type UploadZipResponse = unknown; export type $OpenApiTs = { - '/api/v1/health/health': { + '/v1/health': { get: { res: { /** @@ -37,10 +37,10 @@ export type $OpenApiTs = { }; }; }; - '/api/v1/upload/upload': { + '/v1/upload': { post: { req: { - formData: Body_upload_upload_zip; + formData: Body_upload_zip; }; res: { /** diff --git a/packages/api/src/router/report.ts b/packages/api/src/router/report.ts index b50354d..f36baa0 100644 --- a/packages/api/src/router/report.ts +++ b/packages/api/src/router/report.ts @@ -24,7 +24,7 @@ export const reportRouter = { OpenAPI.TOKEN = token; - const response = await HealthService.healthHealthCheck(); + const response = await HealthService.healthCheck(); return response; }),