feat: added current user validation to apis
Signed-off-by: Varun Raj <mailme@varunraj.in>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
|
||||
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
|
||||
import { IAsset } from "@/types/asset";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import { CalendarArrowDown, CalendarArrowUp, Hourglass } from "lucide-react";
|
||||
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
|
||||
import { listMissingLocationAssets } from "@/handlers/api/asset.handler";
|
||||
import { formatDate, parseDate } from "@/helpers/date.helper";
|
||||
import { addDays } from "date-fns";
|
||||
|
||||
export default function MissingLocationAssets() {
|
||||
const { startDate, selectedIds, assets, updateContext } = useMissingLocationContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
const fetchAssets = async () => {
|
||||
if (!startDate) return;
|
||||
setLoading(true);
|
||||
updateContext({
|
||||
assets: [],
|
||||
})
|
||||
return listMissingLocationAssets({ startDate })
|
||||
.then((assets) => updateContext({ assets }))
|
||||
.catch(setErrorMessage)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const images = useMemo(() => {
|
||||
return assets.map((p) => ({
|
||||
...p,
|
||||
src: p.url as string,
|
||||
original: p.previewUrl as string,
|
||||
width: p.exifImageWidth as number,
|
||||
height: p.exifImageHeight as number,
|
||||
isSelected: selectedIds.includes(p.id),
|
||||
}));
|
||||
}, [assets, selectedIds]);
|
||||
console.log(images)
|
||||
|
||||
const slides = useMemo(
|
||||
() =>
|
||||
images.map(({ original, width, height }) => ({
|
||||
src: original,
|
||||
width,
|
||||
height,
|
||||
})),
|
||||
[images]
|
||||
);
|
||||
|
||||
const handleClick = (idx: number) => setIndex(idx);
|
||||
|
||||
const handleSelect = (_idx: number, asset: IAsset) => {
|
||||
const isPresent =selectedIds.includes(asset.id)
|
||||
if(isPresent){
|
||||
updateContext({selectedIds:selectedIds.filter((id)=>id!==asset.id)})
|
||||
}
|
||||
else{
|
||||
updateContext({selectedIds:[...selectedIds,asset.id]})
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate) fetchAssets();
|
||||
}, [startDate]);
|
||||
|
||||
if (loading) return (
|
||||
<div className="flex flex-col gap-2 h-full justify-center items-center w-full">
|
||||
<Hourglass />
|
||||
<p className="text-lg">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!startDate) return (
|
||||
<div className="flex flex-col gap-2 h-full justify-center items-center w-full">
|
||||
<CalendarArrowUp />
|
||||
<p className="text-lg">Please select a date</p>
|
||||
<p className="text-sm">
|
||||
When you select a date from the left, you will see all the orphan assets captured on that date
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Lightbox
|
||||
slides={slides}
|
||||
plugins={[Captions]}
|
||||
open={index >= 0}
|
||||
index={index}
|
||||
close={() => setIndex(-1)}
|
||||
/>
|
||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<Gallery
|
||||
images={images}
|
||||
onClick={handleClick}
|
||||
enableImageSelection={true}
|
||||
onSelect={handleSelect}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
|
||||
|
||||
import { IMissingLocationDatesResponse } from "@/handlers/api/asset.handler";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
|
||||
interface IProps {
|
||||
record: IMissingLocationDatesResponse;
|
||||
onSelect: (date: string) => void;
|
||||
}
|
||||
export default function MissingLocationDateItem({ record, onSelect }: IProps) {
|
||||
const { startDate } = useMissingLocationContext()
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => onSelect(record.date)}
|
||||
key={record.date}
|
||||
className={
|
||||
cn("flex gap-1 flex-col p-2 py-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100 px-4",
|
||||
startDate === record.date ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "")
|
||||
}
|
||||
>
|
||||
<p className="font-mono text-sm">{record.date}</p>
|
||||
<p className="text-xs">{record.asset_count} W/O Location</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { use, useEffect, useState } from "react";
|
||||
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
|
||||
import { IMissingLocationDatesResponse, listMissingLocationDates } from "@/handlers/api/asset.handler";
|
||||
import MissingLocationDateItem from "./MissingLocationDateItem";
|
||||
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
|
||||
|
||||
export default function MissingLocationDates() {
|
||||
const { updateContext } = useMissingLocationContext();
|
||||
const [dateRecords, setDateRecords] = React.useState<
|
||||
IMissingLocationDatesResponse[]
|
||||
>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
return listMissingLocationDates({})
|
||||
.then(setDateRecords)
|
||||
.catch(setErrorMessage)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const handleSelect = (date: string) => {
|
||||
updateContext({ startDate: date });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto min-w-[170px] py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] dark:bg-zinc-900 bg-gray-200 flex flex-col gap-2 px-2">
|
||||
{dateRecords.map((record) => (
|
||||
<MissingLocationDateItem
|
||||
key={record.date}
|
||||
record={record}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { getMe } from "@/handlers/api/user.handler";
|
||||
import { IUser } from "@/types/user";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
|
||||
export default function ProfileInfo() {
|
||||
const { immichURL } = useConfig();
|
||||
const [user, setUser] = React.useState<IUser | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const fetchData = () => {
|
||||
return getMe()
|
||||
.then(setUser)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (errorMessage) return <div>{errorMessage}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-xs text-center py-2">
|
||||
<p className="text-muted-foreground font-mono">Connected to</p>
|
||||
<Link href={immichURL} target="_blank" className="font-mono">
|
||||
{immichURL}
|
||||
</Link>
|
||||
<p className="text-sm">{user?.name}</p>
|
||||
</div>
|
||||
<div className="border text-muted-foreground text-xs text-center py-2">
|
||||
<p>
|
||||
Made with <span className="text-red-500">♥</span> by{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://x.com/zathvarun"
|
||||
className="text-primary"
|
||||
>
|
||||
@zathvarun
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import { Bell, Camera, Package2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { sidebarNavs } from "@/config/constants/sidebarNavs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter } from "next/router";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { ENV } from "@/config/environment";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from "next/image";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import ProfileInfo from "./ProfileInfo";
|
||||
|
||||
const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Sidebar() {
|
||||
const { immichURL } = useConfig();
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
|
||||
@@ -58,24 +52,8 @@ export default function Sidebar() {
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col gap-1 text-xs text-center py-2">
|
||||
<p className="text-muted-foreground font-mono">Connected to</p>
|
||||
<Link href={immichURL} target="_blank" className="font-mono">
|
||||
{immichURL}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border text-muted-foreground text-xs text-center py-2">
|
||||
<p>
|
||||
Made with <span className="text-red-500">♥</span> by{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://x.com/zathvarun"
|
||||
className="text-primary"
|
||||
>
|
||||
@zathvarun
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<ProfileInfo />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-3
@@ -1,6 +1,8 @@
|
||||
export const BASE_API_ENDPOINT = '/api';
|
||||
export const BASE_PROXY_ENDPOINT = BASE_API_ENDPOINT + '/immich-proxy';
|
||||
|
||||
export const GET_ME_PATH = BASE_PROXY_ENDPOINT + "/users/me";
|
||||
|
||||
export const LIST_PEOPLE_PATH = BASE_API_ENDPOINT + "/people/list";
|
||||
export const SEARCH_PEOPLE_PATH = BASE_PROXY_ENDPOINT + "/search/person";
|
||||
export const PERSON_THUBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/thumbnail/" + id;
|
||||
@@ -13,7 +15,12 @@ export const EXIF_DISTRIBUTION_PATH = (column: string) => BASE_API_ENDPOINT + "/
|
||||
// Albums
|
||||
export const LIST_POTENTIAL_ALBUMS_DATES_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-dates";
|
||||
export const LIST_POTENTIAL_ALBUMS_ASSETS_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-assets";
|
||||
export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id;
|
||||
export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview";
|
||||
export const LIST_ALBUMS_PATH = BASE_PROXY_ENDPOINT + "/albums";
|
||||
export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets";
|
||||
export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets";
|
||||
|
||||
// Assets
|
||||
export const LIST_MISSING_LOCATION_DATES_PATH = BASE_API_ENDPOINT + "/assets/missing-location-dates";
|
||||
export const LIST_MISSING_LOCATION_ASSETS_PATH = BASE_API_ENDPOINT + "/assets/missing-location-assets";
|
||||
|
||||
export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id;
|
||||
export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IAsset } from "@/types/asset";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export interface IMissingLocationConfig {
|
||||
startDate?: string;
|
||||
selectedIds: string[];
|
||||
assets: IAsset[];
|
||||
}
|
||||
|
||||
export interface MissingLocationContext extends IMissingLocationConfig {
|
||||
updateContext: (newConfig: Partial<MissingLocationContext>) => void;
|
||||
}
|
||||
const MissingLocationContext = createContext<MissingLocationContext>({
|
||||
startDate: undefined,
|
||||
selectedIds: [],
|
||||
assets: [],
|
||||
updateContext: () => { },
|
||||
});
|
||||
|
||||
export default MissingLocationContext;
|
||||
|
||||
const useMissingLocationContext = () => {
|
||||
return useContext(MissingLocationContext) as MissingLocationContext;
|
||||
}
|
||||
export { useMissingLocationContext };
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
ADD_ASSETS_ALBUMS_PATH,
|
||||
LIST_ALBUMS_PATH,
|
||||
LIST_MISSING_LOCATION_ASSETS_PATH,
|
||||
LIST_MISSING_LOCATION_DATES_PATH,
|
||||
} from "@/config/routes";
|
||||
import { cleanUpAsset } from "@/helpers/asset.helper";
|
||||
import API from "@/lib/api";
|
||||
import { IAsset } from "@/types/asset";
|
||||
|
||||
interface IMissingAssetAlbumsFilters {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
export interface IMissingLocationDatesResponse {
|
||||
date: string;
|
||||
asset_count: number;
|
||||
}
|
||||
|
||||
export const listMissingLocationDates = async (
|
||||
filters: IMissingAssetAlbumsFilters
|
||||
): Promise<IMissingLocationDatesResponse[]> => {
|
||||
return API.get(LIST_MISSING_LOCATION_DATES_PATH, filters);
|
||||
};
|
||||
|
||||
export const listMissingLocationAssets = async (
|
||||
filters: IMissingAssetAlbumsFilters
|
||||
): Promise<IAsset[]> => {
|
||||
return API.get(LIST_MISSING_LOCATION_ASSETS_PATH, filters).then((assets) =>
|
||||
assets.map(cleanUpAsset)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { GET_ME_PATH } from "@/config/routes";
|
||||
import API from "@/lib/api";
|
||||
|
||||
export const getMe = async () => {
|
||||
return API.get(GET_ME_PATH);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ENV } from "@/config/environment"
|
||||
|
||||
export const getCurrentUser = () => {
|
||||
console.log(ENV.IMMICH_URL + "/api/users/me")
|
||||
return fetch(ENV.IMMICH_URL + "/api/users/me", {
|
||||
headers: {
|
||||
'x-api-key': ENV.IMMICH_API_KEY,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
}
|
||||
throw new Error("Failed to fetch user")
|
||||
})
|
||||
}
|
||||
@@ -6,4 +6,10 @@ export const formatDate = (date: string, outputFormat?: string): string => {
|
||||
|
||||
export const parseDate = (date: string, inputFormat: string): Date => {
|
||||
return parse(date, inputFormat, new Date());
|
||||
}
|
||||
}
|
||||
|
||||
export const addDays = (date: Date, days: number): Date => {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3,58 +3,6 @@ import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
|
||||
|
||||
// const handleErrors = (error: AxiosError | any) => {
|
||||
// console.error('error_message', error.message)
|
||||
// if (error.message === 'canceled') {
|
||||
// const cancelledError = {
|
||||
// code: 'ERR_CANCELED',
|
||||
// message: 'Request canceled',
|
||||
// }
|
||||
// return Promise.reject(cancelledError)
|
||||
// }
|
||||
// let errorObject = {}
|
||||
// if (error.response) {
|
||||
// const { status } = error.response
|
||||
// if (status === 401) {
|
||||
// errorObject = {
|
||||
// ...error.response.data,
|
||||
// message: error.response.data.message,
|
||||
// }
|
||||
// if (typeof window !== 'undefined' && window.location) {
|
||||
// // return resetSession().then(() => {
|
||||
// // window.location.href = '/auth/signin'
|
||||
// // return Promise.reject(errorObject)
|
||||
// // })
|
||||
// }
|
||||
// } else if (status === 403) {
|
||||
// errorObject = {
|
||||
// ...error.response.data,
|
||||
// message:
|
||||
// 'You do not have access to this page. Please contact your administrator!',
|
||||
// }
|
||||
// if (typeof window !== 'undefined' && window.location) {
|
||||
// // deleteLocalHashData()
|
||||
// window.location.href = '/404'
|
||||
// return Promise.reject(errorObject)
|
||||
// }
|
||||
// } else if (status === 422) {
|
||||
// errorObject = {
|
||||
// ...error.response.data,
|
||||
// message: 'Unable to process this request, please try again later',
|
||||
// }
|
||||
// } else {
|
||||
// errorObject = {
|
||||
// ...error.response.data,
|
||||
// }
|
||||
// }
|
||||
// return Promise.reject(errorObject)
|
||||
// }
|
||||
// errorObject = {
|
||||
// message: 'Network Error. Please check your connection and try again!',
|
||||
// }
|
||||
// return Promise.reject(errorObject)
|
||||
// }
|
||||
|
||||
export default class API {
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ import type { AppProps } from "next/app";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ENV } from "@/config/environment";
|
||||
import ConfigContext from "@/contexts/ConfigContext";
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getMe } from "@/handlers/api/user.handler";
|
||||
|
||||
interface AppPropsWithProps extends AppProps {
|
||||
props: {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { exif } from "@/schema";
|
||||
import { count, desc, isNotNull, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const SELECT_ORPHAN_PHOTOS = (date: string) =>
|
||||
const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) =>
|
||||
sql.raw(`
|
||||
SELECT
|
||||
a."id",
|
||||
@@ -35,7 +36,8 @@ const SELECT_ORPHAN_PHOTOS = (date: string) =>
|
||||
ON a.id = e."assetId"
|
||||
WHERE
|
||||
aaa."albumsId" IS NULL
|
||||
AND a."localDateTime"::date = '${date}';
|
||||
AND a."ownerId" = '${ownerId}'
|
||||
AND a."localDateTime"::date = '${date}'
|
||||
`);
|
||||
|
||||
export default async function handler(
|
||||
@@ -43,8 +45,9 @@ export default async function handler(
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const currentUser = await getCurrentUser()
|
||||
const { startDate } = req.query as { startDate: string };
|
||||
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate));
|
||||
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate, currentUser.id));
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { exif } from "@/schema";
|
||||
import { count, desc, isNotNull, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const SELECT_ORPHAN_PHOTOS = sql`
|
||||
const SELECT_ORPHAN_PHOTOS = (ownerId: string) => sql`
|
||||
SELECT
|
||||
DATE(a."localDateTime") AS "date",
|
||||
COUNT(a."id") AS "asset_count"
|
||||
@@ -15,7 +16,8 @@ LEFT JOIN
|
||||
"albums_assets_assets" aaa ON a."id" = aaa."assetsId"
|
||||
WHERE
|
||||
aaa."albumsId" IS NULL
|
||||
GROUP BY
|
||||
AND a."ownerId" = ${ownerId}
|
||||
GROUP BY
|
||||
DATE(a."localDateTime")
|
||||
ORDER BY
|
||||
"asset_count" DESC;
|
||||
@@ -26,7 +28,8 @@ export default async function handler(
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS);
|
||||
const currentUser = await getCurrentUser()
|
||||
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(currentUser.id));
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
import { db } from "@/config/db";
|
||||
import { exif } from "@/schema";
|
||||
import { count, desc, isNotNull, ne } from "drizzle-orm";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { and, count, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const columnMap = {
|
||||
@@ -33,12 +34,17 @@ export default async function handler(
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await getCurrentUser();
|
||||
const dataFromDB = await db.select({
|
||||
value: count(),
|
||||
label: column,
|
||||
})
|
||||
.from(exif)
|
||||
.where(isNotNull(column))
|
||||
.leftJoin(assets, eq(assets.id, exif.assetId))
|
||||
.where(and(
|
||||
isNotNull(column),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
))
|
||||
.limit(20)
|
||||
.groupBy(column).orderBy(desc(count()));
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { parseDate } from "@/helpers/date.helper";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { addDays } from "date-fns";
|
||||
import { and, count, desc, eq, gte, isNotNull, isNull, lte, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { startDate } = req.query as {
|
||||
startDate: string;
|
||||
};
|
||||
|
||||
const currentUser = await getCurrentUser();
|
||||
|
||||
if (!startDate) {
|
||||
return res.status(400).json({
|
||||
error: "startDate and endDate are required",
|
||||
});
|
||||
}
|
||||
|
||||
const startDateDate = parseDate(startDate, "yyyy-MM-dd");
|
||||
const endDateDate = addDays(parseDate(startDate, "yyyy-MM-dd"), 1);
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
previewPath: assets.previewPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
duration: assets.duration,
|
||||
thumbnailPath: assets.thumbnailPath,
|
||||
encodedVideoPath: assets.encodedVideoPath,
|
||||
originalFileName: assets.originalFileName,
|
||||
sidecarPath: assets.sidecarPath,
|
||||
deletedAt: assets.deletedAt,
|
||||
localDateTime: assets.localDateTime,
|
||||
exifImageWidth: exif.exifImageWidth,
|
||||
exifImageHeight: exif.exifImageHeight,
|
||||
ownerId: assets.ownerId,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
isNull(exif.latitude),
|
||||
isNotNull(assets.createdAt),
|
||||
gte(exif.dateTimeOriginal, startDateDate),
|
||||
lte(exif.dateTimeOriginal, endDateDate),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
));
|
||||
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { and, count, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const currentUser = await getCurrentUser();
|
||||
console.log({ currentUser });
|
||||
const rows = await db
|
||||
.select({
|
||||
asset_count: desc(count(assets.id)),
|
||||
date: sql`DATE(${exif.dateTimeOriginal})`,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
isNull(exif.latitude),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
))
|
||||
.groupBy(sql`DATE(${exif.dateTimeOriginal})`)
|
||||
.orderBy(desc(count(assets.id)));
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('HTTP error:', response)
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { stringToBoolean } from "@/helpers/data.helper";
|
||||
import { assetFaces, assets, exif, person } from "@/schema";
|
||||
import {
|
||||
@@ -43,12 +44,15 @@ export default async function handler(
|
||||
sortOrder = "desc",
|
||||
} = req.query as any as IQuery;
|
||||
|
||||
const currentUser = await getCurrentUser();
|
||||
|
||||
const maximumAssetCount = !maxValue || maxValue <= 0 ? 1000000 : maxValue;
|
||||
const whereClause = and(
|
||||
eq(person.isHidden, false),
|
||||
isNull(assets.duplicateId),
|
||||
eq(assets.isVisible, true),
|
||||
eq(assets.isArchived, false),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
stringToBoolean(nameLessOnly) ? eq(person.name, "") : undefined
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import AlbumSelectorDialog from "@/components/albums/AlbumSelectorDialog";
|
||||
import PotentialAlbumsAssets from "@/components/albums/potential-albums/PotentialAlbumsAssets";
|
||||
import PotentialAlbumsDates from "@/components/albums/potential-albums/PotentialAlbumsDates";
|
||||
import MissingLocationAssets from "@/components/assets/missing-location/MissingLocationAssets";
|
||||
import MissingLocationDates from "@/components/assets/missing-location/MissingLocationDates";
|
||||
import PageLayout from "@/components/layouts/PageLayout";
|
||||
import Header from "@/components/shared/Header";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import MissingLocationContext, {
|
||||
IMissingLocationConfig,
|
||||
} from "@/contexts/MissingLocationContext";
|
||||
|
||||
import { addAssetToAlbum } from "@/handlers/api/album.handler";
|
||||
import { IAlbum } from "@/types/album";
|
||||
import React from "react";
|
||||
|
||||
export default function MissingLocations() {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [config, setConfig] = React.useState<IMissingLocationConfig>({
|
||||
startDate: undefined,
|
||||
selectedIds: [],
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const handleSelect = (album: IAlbum) => {
|
||||
return addAssetToAlbum(album.id, config.selectedIds)
|
||||
.then(() => {
|
||||
setConfig({ ...config, selectedIds: [] });
|
||||
setConfig({
|
||||
...config,
|
||||
assets: config.assets.filter(
|
||||
(asset) => !config.selectedIds.includes(asset.id)
|
||||
),
|
||||
selectedIds: [],
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: `Assets added to ${album.albumName}`,
|
||||
description: `${config.selectedIds.length} assets added to album`,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add assets album",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout className="!p-0 !mb-0">
|
||||
<Header
|
||||
leftComponent="Missing Location"
|
||||
rightComponent={
|
||||
<>
|
||||
<Badge variant={"outline"}>
|
||||
{config.selectedIds.length} Selected
|
||||
</Badge>
|
||||
{config.selectedIds.length === config.assets.length ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
Unselect all
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: config.assets.map((a) => a.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
Select all
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<MissingLocationContext.Provider
|
||||
value={{
|
||||
...config,
|
||||
updateContext: (newConfig: Partial<IMissingLocationConfig>) =>
|
||||
setConfig({ ...config, ...newConfig }),
|
||||
}}
|
||||
>
|
||||
<div className="flex divide-y">
|
||||
<MissingLocationDates />
|
||||
<MissingLocationAssets />
|
||||
</div>
|
||||
</MissingLocationContext.Provider>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core'
|
||||
export const assets = pgTable('assets', {
|
||||
id: uuid('id').defaultRandom().primaryKey().notNull(),
|
||||
deviceAssetId: varchar('deviceAssetId').notNull(),
|
||||
// ownerId: uuid('ownerId').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
ownerId: uuid('ownerId').notNull(),
|
||||
deviceId: varchar('deviceId').notNull(),
|
||||
type: varchar('type').notNull(),
|
||||
originalPath: varchar('originalPath').notNull(),
|
||||
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
export interface IUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
profileImagePath: string;
|
||||
avatarColor: string;
|
||||
storageLabel: string;
|
||||
shouldChangePassword: boolean;
|
||||
isAdmin: boolean;
|
||||
createdAt: Date;
|
||||
deletedAt: null;
|
||||
updatedAt: Date;
|
||||
oauthId: string;
|
||||
quotaSizeInBytes: null;
|
||||
quotaUsageInBytes: number;
|
||||
status: string;
|
||||
license: null;
|
||||
}
|
||||
Reference in New Issue
Block a user