+
+
diff --git a/libertas-frontend/src/routes/albums/$albumId.tsx b/libertas-frontend/src/routes/albums/$albumId.tsx
new file mode 100644
index 0000000..f29258e
--- /dev/null
+++ b/libertas-frontend/src/routes/albums/$albumId.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/albums/$albumId")({
+ component: AlbumDetailPage,
+});
+
+function AlbumDetailPage() {
+ const { albumId } = Route.useParams();
+
+ return (
+
+
Album: {albumId}
+
+ This page will show the details and photos for a single album.
+
+ {/* TODO: Fetch album details and display media grid */}
+
+ );
+}
diff --git a/libertas-frontend/src/routes/albums/index.tsx b/libertas-frontend/src/routes/albums/index.tsx
new file mode 100644
index 0000000..f7c9f42
--- /dev/null
+++ b/libertas-frontend/src/routes/albums/index.tsx
@@ -0,0 +1,34 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { useGetAlbums } from "@/features/albums/use-albums";
+import { AlbumCard } from "@/components/albums/album-card";
+import { CreateAlbumDialog } from "@/components/albums/create-album-dialog";
+import { Separator } from "@/components/ui/separator";
+
+export const Route = createFileRoute("/albums/")({
+ component: AlbumsPage,
+});
+
+function AlbumsPage() {
+ const { data: albums, isLoading, error } = useGetAlbums();
+
+ return (
+
+
+
Albums
+
+
+
+
+ {isLoading &&
Loading albums...
}
+ {error &&
Error loading albums: {error.message}
}
+
+ {albums && (
+
+ {albums.map((album) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/libertas-frontend/src/routes/media/index.tsx b/libertas-frontend/src/routes/media/index.tsx
new file mode 100644
index 0000000..f80f8ec
--- /dev/null
+++ b/libertas-frontend/src/routes/media/index.tsx
@@ -0,0 +1,72 @@
+import { useGetMediaList } from "@/features/media/use-media";
+import { createFileRoute } from "@tanstack/react-router";
+import { Button } from "@/components/ui/button";
+import { AuthenticatedImage } from "@/components/media/authenticated-image";
+import type { Media } from "@/domain/types";
+import { useState } from "react";
+import { MediaViewer } from "@/components/media/media-viewer";
+
+export const Route = createFileRoute("/media/")({
+ component: MediaPage,
+});
+
+function MediaPage() {
+ const {
+ data,
+ isLoading,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useGetMediaList();
+
+ const [selectedMedia, setSelectedMedia] = useState
(null);
+
+ return (
+
+
+
All Photos
+
+
+ {isLoading &&
Loading photos...
}
+ {error &&
Error loading photos: {error.message}
}
+
+ {data && (
+
+ {data.pages.map((page) =>
+ page.data.map((media) => (
+
setSelectedMedia(media)}
+ >
+
+
+ ))
+ )}
+
+ )}
+
+ {hasNextPage && (
+
+
+
+ )}
+
+
{
+ if (!open) {
+ setSelectedMedia(null);
+ }
+ }}
+ />
+
+ );
+}
diff --git a/libertas-frontend/src/routes/people/index.tsx b/libertas-frontend/src/routes/people/index.tsx
new file mode 100644
index 0000000..7705413
--- /dev/null
+++ b/libertas-frontend/src/routes/people/index.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/people/")({
+ component: PeoplePage,
+});
+
+function PeoplePage() {
+ return (
+
+
People
+
+ This is where you'll see all the people identified in your photos.
+
+ {/* TODO: Add 'Cluster Faces' button */}
+
+ );
+}
diff --git a/libertas-frontend/src/services/album-service.ts b/libertas-frontend/src/services/album-service.ts
new file mode 100644
index 0000000..17379c1
--- /dev/null
+++ b/libertas-frontend/src/services/album-service.ts
@@ -0,0 +1,26 @@
+import type { Album } from "@/domain/types"
+import apiClient from "@/services/api-client"
+
+export type CreateAlbumPayload = {
+ name: string
+ description?: string
+}
+
+/**
+ * Fetches a list of albums.
+ * TODO: This should become paginated later.
+ */
+export const getAlbums = async (): Promise => {
+ const { data } = await apiClient.get('/albums')
+ return data
+}
+
+/**
+ * Creates a new album.
+ */
+export const createAlbum = async (
+ payload: CreateAlbumPayload,
+): Promise => {
+ const { data } = await apiClient.post('/albums', payload)
+ return data
+}
\ No newline at end of file
diff --git a/libertas-frontend/src/services/api-client.ts b/libertas-frontend/src/services/api-client.ts
index 3e6418f..ade9cab 100644
--- a/libertas-frontend/src/services/api-client.ts
+++ b/libertas-frontend/src/services/api-client.ts
@@ -3,7 +3,7 @@ import { useAuthStorage } from '@/hooks/use-auth-storage'
const apiClient = axios.create({
- baseURL: 'http://localhost:8080/api/v1',
+ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1',
})
apiClient.interceptors.request.use(
diff --git a/libertas-frontend/src/services/media-service.ts b/libertas-frontend/src/services/media-service.ts
new file mode 100644
index 0000000..7ff8d8a
--- /dev/null
+++ b/libertas-frontend/src/services/media-service.ts
@@ -0,0 +1,56 @@
+import type { Media, PaginatedResponse } from "@/domain/types"
+import apiClient from "@/services/api-client"
+
+type MediaListParams = {
+ page: number
+ limit: number
+}
+
+/**
+ * Fetches a paginated list of media.
+ */
+export const getMediaList = async ({
+ page,
+ limit,
+}: MediaListParams): Promise> => {
+ const { data } = await apiClient.get('/media', {
+ params: { page, limit },
+ })
+
+ // we need to append base url to file_url and thumbnail_url
+ const prefix = import.meta.env.VITE_PREFIX_PATH || apiClient.defaults.baseURL;
+
+ data.data = data.data.map((media: Media) => ({
+ ...media,
+ file_url: `${prefix}${media.file_url}`,
+ thumbnail_url: media.thumbnail_url
+ ? `${prefix}${media.thumbnail_url}`
+ : null,
+ }))
+
+ return data
+}
+
+/**
+ * Uploads a new media file.
+ */
+export const uploadMedia = async (
+ file: File,
+ onProgress: (progress: number) => void,
+): Promise => {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ const { data } = await apiClient.post('/media', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ const percentCompleted = Math.round(
+ (progressEvent.loaded * 100) / (progressEvent.total ?? 100),
+ )
+ onProgress(percentCompleted)
+ },
+ })
+ return data
+}
\ No newline at end of file