This commit is contained in:
Rizky 2023-10-14 15:17:49 +07:00
parent 02727d17fd
commit e9b306e9a2
12 changed files with 143 additions and 979 deletions

72
app/srv/api/auth/login.ts Normal file
View File

@ -0,0 +1,72 @@
import { apiContext } from "service-srv";
import argon from "@node-rs/argon2";
import { session } from "utils/session";
export const _ = {
url: "/_login",
async api(username: string, password: string) {
const { res, req } = apiContext(this);
const current = session.get(req);
if (!current) {
const user = await db.user.findFirst({
where: { OR: [{ username }, { phone: username }] },
include: {
org_user: {
select: {
org: {
select: {
id: true,
name: true,
},
},
},
},
org: {
select: { id: true, name: true },
},
},
});
if (user && user.org_user) {
user.org = user.org_user.map((e) => e.org);
delete (user as any).org_user;
}
try {
if (!!user && (await argon.verify(user.password, password))) {
//@ts-ignore
delete user.password;
const sdata = await session.new({ user });
let setDefaultCookie = true;
const origin = req.headers.get("origin");
if (origin) {
const url = new URL(origin);
if (url.hostname === "localhost") {
setDefaultCookie = false;
res.setHeader("set-cookie", `${session.cookieKey}=${sdata.id};`);
}
}
if (setDefaultCookie) {
res.setHeader(
"set-cookie",
`${session.cookieKey}=${sdata.id}; SameSite=None; Secure; HttpOnly`
);
}
return { status: "ok", session: sdata };
}
} catch (e) {
console.error(e, user, password);
}
} else {
return { status: "ok", session: current };
}
return {
status: "failed",
reason: "Invalid username / password",
};
},
};

View File

@ -1,9 +1,37 @@
import { apiContext } from "service-srv"; import { apiContext } from "service-srv";
import { session } from "utils/session";
import { user } from "dbgen";
export const _ = { export const _ = {
url: "/session)}", url: "/session",
async api() { async api() {
const { req, res } = apiContext(this); const { req, res } = apiContext(this);
return "This is session.ts"; const sdata = session.get<{
user: user & {
org: {
id: string;
name: string;
}[];
};
}>(req);
if (sdata) {
let setDefaultCookie = true;
const origin = req.headers.get("origin");
if (origin) {
const url = new URL(origin);
if (url.hostname === "localhost") {
setDefaultCookie = false;
res.setHeader("set-cookie", `${session.cookieKey}=${sdata.id};`);
} }
} }
if (setDefaultCookie) {
res.setHeader(
"set-cookie",
`${session.cookieKey}=${sdata.id}; SameSite=None; Secure; HttpOnly`
);
}
}
return sdata;
},
};

23
app/srv/exports.d.ts vendored
View File

@ -1,10 +1,31 @@
declare module "api/auth/login" {
export const _: {
url: string;
api(username: string, password: string): Promise<{
status: string;
session: any;
reason?: undefined;
} | {
status: string;
reason: string;
session?: undefined;
}>;
};
}
declare module "api/session" { declare module "api/session" {
export const _: { export const _: {
url: string; url: string;
api(): Promise<string>; api(): Promise<any>;
}; };
} }
declare module "exports" { declare module "exports" {
export const login: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("api/auth/login")>;
};
export const session: { export const session: {
name: string; name: string;
url: string; url: string;

View File

@ -1,6 +1,13 @@
export const login = {
name: "login",
url: "/_login",
path: "app/srv/api/auth/login.ts",
args: ["username","password"],
handler: import("./api/auth/login")
}
export const session = { export const session = {
name: "session", name: "session",
url: "/session)}", url: "/session",
path: "app/srv/api/session.ts", path: "app/srv/api/session.ts",
args: [], args: [],
handler: import("./api/session") handler: import("./api/session")

View File

@ -1,7 +1,6 @@
import { FC } from "react"; import { FC } from "react";
import { useLocal } from "web-utils"; import { useLocal } from "web-utils";
import { FieldColor } from "../ui/FieldColor"; import { FieldColor } from "../ui/FieldColor";
import { FieldImg } from "../ui/FieldImg";
import { dropdownProp } from "../ui/style"; import { dropdownProp } from "../ui/style";
import { Button } from "../ui/Button"; import { Button } from "../ui/Button";
import { FNBackground } from "../../../../../utils/types/meta-fn"; import { FNBackground } from "../../../../../utils/types/meta-fn";
@ -53,81 +52,6 @@ export const PanelBackground: FC<{
/> />
</div> </div>
</Tooltip> </Tooltip>
<Tooltip asChild content={"Background Image"}>
<div
className={cx(
"bg-white p-[2px] border flex flex-1 border-gray-300",
css`
> * {
flex: 1;
}
`
)}
>
<FieldImg
value={bg.url}
update={async (url) => {
update("bg", { ...bg, url });
if (
value.type === "item" &&
(!value.dim ||
(value.dim && (!value.dim.w || value.dim.w === "fit")))
) {
const img = await getImgMeta(`${siteApiUrl}${url}`);
(update as any)("dim", {
w: Math.min(500, img?.width || 100),
h: Math.min(500, img?.height || 100),
});
}
}}
/>
</div>
</Tooltip>
{bg.url ? (
<>
<Tooltip asChild content={"Unlink Image"}>
<div className={"flex flex-row bg-white"}>
<Button
className={cx(
"flex-1 flex-grow",
css`
height: 30px;
width: 20px;
max-width: 20px;
padding: 0px !important;
min-width: 0px !important;
`
)}
onClick={(e) => {
update("bg", { ...bg, url: "" });
}}
>
<div className="w-[10px] flex items-center justify-center">
<>
<div className="text-lg text-gray-700">
<svg
width="15"
height="15"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M16.949 14.121L19.071 12a5.008 5.008 0 0 0 0-7.071a5.006 5.006 0 0 0-7.071 0l-.707.707l1.414 1.414l.707-.707a3.007 3.007 0 0 1 4.243 0a3.005 3.005 0 0 1 0 4.243l-2.122 2.121a2.723 2.723 0 0 1-.844.57L13.414 12l1.414-1.414l-.707-.707a4.965 4.965 0 0 0-3.535-1.465c-.235 0-.464.032-.691.066L3.707 2.293L2.293 3.707l18 18l1.414-1.414l-5.536-5.536c.277-.184.538-.396.778-.636zm-6.363 3.536a3.007 3.007 0 0 1-4.243 0a3.005 3.005 0 0 1 0-4.243l1.476-1.475l-1.414-1.414L4.929 12a5.008 5.008 0 0 0 0 7.071a4.983 4.983 0 0 0 3.535 1.462A4.982 4.982 0 0 0 12 19.071l.707-.707l-1.414-1.414l-.707.707z"
/>
</svg>
</div>
</>
</div>
</Button>
</div>
</Tooltip>
</>
) : (
<></>
)}
</div> </div>
<div className="flex items-stretch space-x-2"> <div className="flex items-stretch space-x-2">

View File

@ -1,43 +0,0 @@
import { FC, useEffect } from "react";
import { useLocal } from "web-utils";
import { FilePicker } from "./FilePicker";
import { Button } from "./Button";
import { FileImageGallery } from "./FileImageGallery";
export const FieldImg: FC<{
value?: string;
update: (value: string) => void;
}> = ({ value, update }) => {
const local = useLocal({ val: "", open: false });
useEffect(() => {
local.val = value || "";
local.render();
}, [value]);
return (
<>
<Button
className="btn-hover h-[22px]"
appearance="subtle"
onClick={() => {
local.open = true;
local.render();
}}
>
Image
</Button>
{local.open && (
<FileImageGallery
value={value}
update={update}
meta={local}
onClose={() => {
local.open = false;
local.render();
}}
/>
)}
</>
);
};

View File

@ -1,421 +0,0 @@
import { FC } from "react";
import { fetchSendApi } from "web-utils/src/web/iframe-cors";
import { useGlobal, useLocal } from "web-utils";
import { Gallery } from "./Gallery";
import { Loading } from "../../../../../utils/ui/loading";
import { ToolbarBox } from "../../../../../utils/ui/box";
import { EditorGlobal } from "../../../logic/global";
export const FileImageGallery: FC<{
value?: string;
update: (src: string) => void;
onClose: () => void;
accept?: string;
type?: string;
meta?: any;
}> = ({
meta,
onClose,
update,
value,
accept = "video/mp4, image/jpeg, image/png, image/jpg, image/x-icon, image/vnd.microsoft.icon",
type = "image",
}) => {
const p = useGlobal(EditorGlobal, "EDITOR");
const local = useLocal(
{
value: value || "",
load: true,
mode: "upload",
preview: false as boolean,
previewUrl: "" as string,
isUpload: false as boolean,
selectUrl: "" as string,
},
async () => {
local.mode = "gallery";
if (local.previewUrl) {
local.selectUrl = local.previewUrl;
}
local.render();
}
);
const onUpload: React.ChangeEventHandler<HTMLInputElement> = async function (
e
) {
local.isUpload = true;
local.render();
const files = e.currentTarget.files;
if (files && p.page) {
const res: string[] = await fetchSendApi(
`${siteApiUrl}/_upload/${p.page.id}`,
files[0]
);
local.previewUrl = res[0];
local.preview = true;
local.selectUrl = local.previewUrl;
local.render();
}
};
return (
<>
<div
className="fixed inset-0 bg-black bg-opacity-10 cursor-pointer"
onClick={() => {
meta.open = false;
meta.render();
}}
></div>
<div className="fixed inset-[50px] bg-white shadow-2xl z-50 ">
{false ? (
<div className="flex w-full h-full items-center justify-center">
<Loading note="img-gallery" backdrop={false} />
</div>
) : (
<div
className={cx(
"relative w-full h-full flex flex-col items-stretch",
css`
contain: content;
overflow: auto;
> ul {
width: 100%;
}
.row {
display: flex;
flex-direction: column;
align-items: stretch;
}
.dropping {
background: #efefff;
}
`
)}
>
<div className="w-full flex flex-row">
<div
className={cx(
"toolbar",
"flex flex-row flex-grow px-2 justify-center items-center"
)}
>
<div
className={cx(
"toolbar-right",
"flex mr-2 ",
css`
align-items: center !important;
border-bottom: 0px !important;
height: auto !important;
.toolbar-box {
height: 22px;
// margin: 0px !important;
}
`
)}
>
<ToolbarBox
items={[
{
onClick() {
local.mode = "upload";
local.render();
},
className: cx(
local.mode === "upload" &&
"border-b-2 border-blue-500 bg-blue-50"
),
content: (
<>
<div className="flex flex-row items-center justify-center space-x-2">
<div className="text-lg text-gray-700">
<svg
width="15"
height="15"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="m19.21 12.04l-1.53-.11l-.3-1.5A5.484 5.484 0 0 0 12 6C9.94 6 8.08 7.14 7.12 8.96l-.5.95l-1.07.11A3.99 3.99 0 0 0 2 14c0 2.21 1.79 4 4 4h13c1.65 0 3-1.35 3-3c0-1.55-1.22-2.86-2.79-2.96zm-5.76.96v3h-2.91v-3H8l4-4l4 4h-2.55z"
opacity=".3"
/>
<path
fill="currentColor"
d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5c0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4c0-2.05 1.53-3.76 3.56-3.97l1.07-.11l.5-.95A5.469 5.469 0 0 1 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5l1.53.11A2.98 2.98 0 0 1 22 15c0 1.65-1.35 3-3 3zM8 13h2.55v3h2.9v-3H16l-4-4z"
/>
</svg>
</div>
<span>Upload File</span>
</div>
</>
),
},
{
onClick() {
local.mode = "gallery";
local.render();
},
className: cx(
local.mode === "gallery" &&
"border-b-2 border-blue-500 bg-blue-50"
),
content: (
<>
<div className="flex flex-row items-center justify-center space-x-2">
<div className="text-lg text-gray-700">
<svg
width="15"
height="15"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="currentColor">
<path d="M18.512 10.077c0 .738-.625 1.337-1.396 1.337c-.77 0-1.395-.599-1.395-1.337c0-.739.625-1.338 1.395-1.338s1.396.599 1.396 1.338Z" />
<path
fillRule="evenodd"
d="M18.036 5.532c-1.06-.137-2.414-.137-4.123-.136h-3.826c-1.71 0-3.064 0-4.123.136c-1.09.14-1.974.437-2.67 1.104S2.29 8.149 2.142 9.195C2 10.21 2 11.508 2 13.147v.1c0 1.64 0 2.937.142 3.953c.147 1.046.456 1.892 1.152 2.559c.696.667 1.58.963 2.67 1.104c1.06.136 2.414.136 4.123.136h3.826c1.71 0 3.064 0 4.123-.136c1.09-.14 1.974-.437 2.67-1.104s1.005-1.514 1.152-2.559C22 16.184 22 14.886 22 13.248v-.1c0-1.64 0-2.937-.142-3.953c-.147-1.046-.456-1.892-1.152-2.559c-.696-.667-1.58-.963-2.67-1.104ZM6.15 6.858c-.936.12-1.475.346-1.87.724c-.393.377-.629.894-.755 1.791c-.1.72-.123 1.619-.128 2.795l.47-.395c1.125-.942 2.819-.888 3.875.124l3.99 3.825a1.2 1.2 0 0 0 1.491.124l.278-.187a3.606 3.606 0 0 1 4.34.25l2.407 2.077c.098-.264.173-.579.227-.964c.128-.916.13-2.124.13-3.824c0-1.7-.002-2.909-.13-3.825c-.126-.897-.362-1.414-.756-1.791c-.393-.378-.933-.604-1.869-.724c-.956-.124-2.216-.125-3.99-.125h-3.72c-1.774 0-3.034.001-3.99.125Z"
clipRule="evenodd"
/>
<path
d="M17.087 2.61c-.86-.11-1.955-.11-3.32-.11h-3.09c-1.364 0-2.459 0-3.318.11c-.89.115-1.633.358-2.222.92a2.9 2.9 0 0 0-.724 1.12c.504-.23 1.074-.366 1.714-.45c1.085-.14 2.47-.14 4.22-.14h3.915c1.749 0 3.134 0 4.219.14c.559.073 1.064.186 1.52.366a2.875 2.875 0 0 0-.693-1.035c-.589-.563-1.331-.806-2.221-.92Z"
opacity=".5"
/>
</g>
</svg>
</div>
<span>Gallery</span>
</div>
</>
),
},
]}
/>
{/* <ResponsiveToggle /> */}
</div>
</div>
</div>
<div className="flex-grow flex flex-col">
{local.mode === "gallery" ? (
<>
<Gallery value={value} update={update} meta={local} />
</>
) : (
<>
<div className="flex flex-row items-center relative px-2 py-4 space-x-4 justify-center flex-1">
<div className="relative flex flex-row p-2 cursor-pointer bg-blue-500 text-white">
<span>Upload Image</span>
<input
type="file"
name="file"
className={cx(
"absolute inset-0 opacity-0 cursor-pointer w-full h-full"
)}
accept={accept}
onChange={onUpload}
/>
</div>
</div>
<div className="flex flex-row flex-grow items-center justify-center px-2 py-4 space-x-4">
{local.preview ? (
<>
<div
className={cx(
"border-2 rounded mx-2 flex flex-row flex-grow h-full items-center justify-center px-2 py-4 space-x-4",
css`
background-image: linear-gradient(
45deg,
#d1d1d1 25%,
transparent 25%
),
linear-gradient(
-45deg,
#d1d1d1 25%,
transparent 25%
),
linear-gradient(
45deg,
transparent 75%,
#d1d1d1 75%
),
linear-gradient(
-45deg,
transparent 75%,
#d1d1d1 75%
);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px,
-10px 0px;
`
)}
>
<a
href={`${siteApiUrl}${local.previewUrl}`}
className={cx(
"bg-no-repeat bg-contain bg-center rounded mx-2 flex flex-row flex-grow h-full items-center justify-center px-2 py-4 space-x-4",
css`
background-image: url("${siteApiUrl}${local.previewUrl}");
`
)}
></a>
</div>
</>
) : (
<>
{local.isUpload ? (
<>
<div
className={cx(
"border-2 rounded mx-2 flex flex-row flex-grow h-full items-center justify-center px-2 py-4 space-x-4"
)}
>
<div className="flex flex-col space-y-2">
<div className="text-lg text-gray-700">
<svg
width="150"
height="150"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<mask id="lineMdCloudUploadOutlineLoop0">
<g fill="#fff">
<circle cx="12" cy="10" r="6" />
<rect
width="9"
height="8"
x="8"
y="12"
/>
<rect
width="17"
height="12"
x="1"
y="8"
rx="6"
>
<animate
attributeName="x"
dur="24s"
repeatCount="indefinite"
values="1;0;1;2;1"
/>
</rect>
<rect
width="17"
height="10"
x="6"
y="10"
rx="5"
>
<animate
attributeName="x"
dur="15s"
repeatCount="indefinite"
values="6;5;6;7;6"
/>
</rect>
</g>
<circle cx="12" cy="10" r="4" />
<rect width="8" height="8" x="8" y="10" />
<rect
width="11"
height="8"
x="3"
y="10"
rx="4"
>
<animate
attributeName="x"
dur="24s"
repeatCount="indefinite"
values="3;2;3;4;3"
/>
</rect>
<rect
width="13"
height="6"
x="8"
y="12"
rx="3"
>
<animate
attributeName="x"
dur="15s"
repeatCount="indefinite"
values="8;7;8;9;8"
/>
</rect>
<g fill="#fff">
<rect
width="3"
height="4"
x="10.5"
y="12"
/>
<path d="M12 9L16 13H8L12 9Z">
<animateMotion
calcMode="linear"
dur="1.5s"
keyPoints="0;0.25;0.5;0.75;1"
keyTimes="0;0.1;0.5;0.8;1"
path="M0 0v-1v2z"
repeatCount="indefinite"
/>
</path>
</g>
</mask>
<rect
width="24"
height="24"
fill="currentColor"
mask="url(#lineMdCloudUploadOutlineLoop0)"
/>
</svg>
</div>
<div className="flex flex-row space-x-2 items-center justify-center text-xl">
<span className="flex flex-row space-x-2">
Uploading...
</span>
{/* <span className="flex flex-row space-x-2 font-medium">
10%
</span> */}
</div>
</div>
</div>
</>
) : (
<></>
)}
</>
)}
</div>
</>
)}
</div>
{local.selectUrl && (
<div className="border-t-2 flex flex-row items-center justify-end p-4">
<div
onClick={() => {
// FieldImg;
update(local.selectUrl);
meta.open = false;
meta.render();
}}
className="relative flex flex-row p-2 cursor-pointer bg-blue-500 text-white font-medium px-6"
>
<span>Choose Image</span>
</div>
</div>
)}
</div>
)}
</div>
</>
);
};

View File

@ -1,147 +0,0 @@
import { FC, useTransition } from "react";
import { fetchSendApi } from "web-utils/src/web/iframe-cors";
import { useLocal } from "web-utils";
import { Button } from "./Button";
export const FilePicker: FC<{
value?: string;
update: (src: string) => void;
onClose: () => void;
accept?: string;
type?: string;
}> = ({
onClose,
update,
value,
accept = "video/mp4, image/jpeg, image/png, image/jpg, image/x-icon, image/vnd.microsoft.icon",
type = "image",
}) => {
const local = useLocal({
value: value || "",
load: true,
});
const [_, tx] = useTransition();
const onUpload: React.ChangeEventHandler<HTMLInputElement> = async function (
e
) {
const files = e.currentTarget.files;
if (files) {
// const res: string[] = (await fetchSendApi(
// `${siteApiUrl}/_upload${site ? `/${site.id}` : ""}`,
// files[0]
// )) as any;
// if (res) {
// const val = res[0];
// local.value = val;
// local.render();
// update(val);
// }
// onClose();
}
};
return null;
return (
<>
<div className="flex flex-col items-stretch space-y-2 flex-1">
<div className="flex-1 flex flex-col relative">
<div className="absolute inset-0 flex overflow-hidden pointer-events-none items-center text-center justify-center">
{local.load && local.value && (
<div className={cx("self-center")}>Loading</div>
)}
{!!local.value ? (
<>
{type === "image" && (
<img
src={
local.value.startsWith("http")
? local.value
: `${siteApiUrl}${local.value}?w=500&h=500&fit=contain`
}
className={cx(
css`
visibility: "visible";
max-height: 100%;
max-width: 100%;
`,
"self-center"
)}
onLoad={() => {
local.load = false;
local.render();
}}
/>
)}
{type === "pdf" && (
<div className=" text-9xl text-green-600 pb-8">
<PdfDocument />
</div>
)}
</>
) : (
<div className="flex flex-col items-center">
<span>Please Upload Image</span>
<i className="italic"> &mdash; or &mdash;</i>
<span>Fill Image URL</span>
</div>
)}
</div>
<input
type="file"
name="file"
className={cx("absolute inset-0 opacity-0")}
accept={accept}
onChange={onUpload}
/>
</div>
<input
value={local.value}
spellCheck={false}
placeholder="Image URL"
onChange={(e) => {
local.value = e.currentTarget.value;
local.render();
tx(() => {
update(local.value);
});
}}
/>
<div className="flex justify-between">
<input type="file" name="file" accept={accept} onChange={onUpload} />
<Button
onClick={() => {
local.value = "";
local.render();
tx(() => {
update(local.value);
});
}}
>
Clear
</Button>
</div>
</div>
</>
);
};
const PdfDocument = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="11"
fill="none"
viewBox="0 0 100 125"
>
{" "}
<path
fill="#000"
fillRule="evenodd"
d="M58.993 32A2.992 2.992 0 0156 29.007V4H19.666A3.665 3.665 0 0016 7.668v82.664A3.674 3.674 0 0019.68 94h60.64A3.683 3.683 0 0084 90.322V32H58.993zm6.919 35.403c-.544.274-1.36.549-2.448.549-2.176 0-5.44-.549-8.16-1.92-4.624.549-8.16 1.097-10.88 2.194-.272 0-.272 0-.544.274-3.264 5.758-5.984 8.5-8.16 8.5-.544 0-.816 0-1.088-.274l-1.36-.823v-.274C33 75.081 33 74.806 33 74.259c.272-1.372 1.904-3.84 5.168-5.759.544-.274 1.36-.823 2.448-1.371.816-1.37 1.632-3.016 2.72-4.935 1.36-2.742 2.176-5.484 2.992-7.952-1.088-3.29-1.632-5.21-.544-9.048C46.056 44.097 46.872 43 47.96 43h.544c.544 0 1.088.274 1.632.548 1.904 1.92 1.088 6.307 0 9.871v.275c1.088 3.016 2.72 5.483 4.352 7.129.816.548 1.36 1.096 2.448 1.645 1.36 0 2.448-.274 3.536-.274 3.264 0 5.44.548 6.256 1.919.272.548.272 1.097.272 1.645-.272.274-.544 1.097-1.088 1.645zM48.232 56.71c-.544 1.919-1.632 4.113-2.72 6.58-.544 1.097-1.088 1.92-1.632 3.016h.544c3.536-1.37 6.8-2.193 8.976-2.467-.544-.274-.816-.549-1.088-.823-1.36-1.645-2.992-3.839-4.08-6.306zm16.592 8.5c-.272-.275-1.36-1.097-5.168-1.097h-.544v.274c1.904.823 3.808 1.371 5.168 1.371H65.096v-.274s-.272 0-.272-.274zM39.8 69.323c-.544.274-1.088.548-1.36.822-1.904 1.645-3.264 3.565-3.536 4.387 1.632-.274 3.264-1.92 4.896-5.21zm8.16-18.92c.272-1.097.544-1.645.544-2.468v-.548c.272-1.37.272-2.468 0-2.742v-.274l-.272-.274s0 .274-.272.274c-.544 1.645-.544 3.564 0 6.032zM84 28H62.005A2.005 2.005 0 0160 25.995V4l24 24z"
></path>
</svg>
);

View File

@ -1,268 +0,0 @@
import { format } from "date-fns";
import get from "lodash.get";
import { FC, useTransition } from "react";
import { fetchSendApi } from "web-utils/src/web/iframe-cors";
import { useGlobal, useLocal } from "web-utils";
import { Loading } from "../../../../../utils/ui/loading";
import { EditorGlobal } from "../../../logic/global";
export const Gallery: FC<{
value?: string;
update: (src: string) => void;
onClose?: () => void;
accept?: string;
type?: string;
meta?: any;
}> = ({
meta,
onClose,
update,
value,
accept = "video/mp4, image/jpeg, image/png, image/jpg, image/x-icon, image/vnd.microsoft.icon",
type = "image",
}) => {
const p = useGlobal(EditorGlobal, "EDITOR");
const local = useLocal(
{
value: value || "",
load: true,
mode: "upload",
list: [] as Array<any>,
ready: false,
isPreview: false,
hover: null as any,
preview: {
url: "" as string,
dimension: {
width: 0 as number,
height: 0 as number,
},
details: null as any,
},
},
async () => {
local.isPreview = false;
local.ready = false;
local.render();
if (p.page) {
let res = (await fetchSendApi(
`${siteApiUrl}/get-gallery/${p.page.id}`,
{}
)) as any;
local.list = res.data;
}
local.ready = true;
local.render();
}
);
const [_, tx] = useTransition();
const getSize = (size: number) => {
let hz = "";
if (size < 1024) hz = size + " B";
else if (size < 1024 * 1024) hz = (size / 1024).toFixed(2) + " KB";
else if (size < 1024 * 1024 * 1024)
hz = (size / 1024 / 1024).toFixed(2) + " MB";
else hz = (size / 1024 / 1024 / 1024).toFixed(2) + " GB";
return hz;
};
const loadImage: any = (imageSrc: any) =>
new Promise((resolve) => {
const image = new Image();
image.onload = () => {
const height = image.height;
const width = image.width;
resolve({ image, width, height });
};
image.src = imageSrc;
});
const onUpload: React.ChangeEventHandler<HTMLInputElement> = async function (
e
) {
const files = e.currentTarget.files;
if (files) {
}
};
return (
<>
{local.ready ? (
<>
<div className="flex flex-row flex-grow w-full">
<div className="flex flex-row flex-grow">
<div className="flex-row flex p-2 relative flex-grow overflow-auto">
<div
className={cx(
"flex items-start flex-wrap pl-[10px] absolute w-full h-full top-0 left-0"
)}
>
{" "}
{local.list.length ? (
<>
{local.list.map((e, idx) => {
const bgurl = `${siteApiUrl}${get(e, "url")}`;
return (
<div
key={e.url}
className={cx(
"relative flex flex-col items-start w-[200px] h-[100px] p-2 text-sm cursor-pointer hover:bg-blue-100 ml-1 mb-1",
"justify-between transition-all",
"hover:border-blue-500",
"bg-no-repeat bg-cover bg-center",
local.preview.url ===
`${siteApiUrl}${get(e, "url")}`
? "border-4 border-blue-500 shadow-xl"
: "border",
css`
background-image: url("${bgurl}");
.edit {
display: none;
}
&:hover {
.edit {
display: flex;
}
}
`
)}
onClick={async () => {
const { width, height } = await loadImage(
`${siteApiUrl}${get(e, "url")}`
);
local.preview = {
url: `${siteApiUrl}${get(e, "url")}`,
dimension: {
width,
height,
},
details: e,
};
local.isPreview = true;
meta.selectUrl = `${siteApiUrl}${get(e, "url")}`;
meta.render();
}}
onMouseEnter={() => {
local.hover = `${siteApiUrl}${get(e, "url")}`;
local.render();
}}
onMouseLeave={() => {
local.hover = null;
local.render();
}}
>
<div className="flex flex-row flex-grow items-end justify-end">
{local.hover ===
`${siteApiUrl}${get(e, "url")}` ? (
<>
{" "}
<div
onClick={async (ev) => {
ev.preventDefault();
ev.stopPropagation();
await fetchSendApi(
`${siteApiUrl}/_delete`,
{
path: get(e, "path"),
}
);
let list = local.list.filter(
(x) => get(x, "path") !== get(e, "path")
);
local.list = list;
local.render();
}}
className="relative flex flex-row p-2 cursor-pointer bg-red-500 rounded font-medium"
>
<div className="text-lg text-gray-700">
<svg
width="15"
height="15"
viewBox="0 0 28 28"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M11.5 6h5a2.5 2.5 0 0 0-5 0ZM10 6a4 4 0 0 1 8 0h6.25a.75.75 0 0 1 0 1.5h-1.31l-1.217 14.603A4.25 4.25 0 0 1 17.488 26h-6.976a4.25 4.25 0 0 1-4.235-3.897L5.06 7.5H3.75a.75.75 0 0 1 0-1.5H10ZM7.772 21.978a2.75 2.75 0 0 0 2.74 2.522h6.976a2.75 2.75 0 0 0 2.74-2.522L21.436 7.5H6.565l1.207 14.478ZM11.75 11a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-1.5 0v-8.5a.75.75 0 0 1 .75-.75Zm5.25.75a.75.75 0 0 0-1.5 0v8.5a.75.75 0 0 0 1.5 0v-8.5Z"
/>
</svg>
</div>
</div>
</>
) : (
<></>
)}
</div>
</div>
);
})}
</>
) : (
<>No Image</>
)}
</div>
</div>
</div>
{local.isPreview ? (
<>
<div className="border-l-2 w-1/4 flex flex-col">
<a
href={`${local.preview.url}`}
className={cx(
"border-2 rounded m-2 flex flex-row items-center justify-center space-x-4 h-[200px]",
css`
background-image: linear-gradient(
45deg,
#d1d1d1 25%,
transparent 25%
),
linear-gradient(-45deg, #d1d1d1 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #d1d1d1 75%),
linear-gradient(-45deg, transparent 75%, #d1d1d1 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
`
)}
>
<div
className={cx(
"bg-no-repeat bg-contain bg-center rounded flex flex-row flex-grow h-full items-center justify-center space-x-4",
css`
background-image: url("${local.preview.url}");
`
)}
></div>
</a>
<p className="text-sm px-2">
Dimension:{" "}
<span>{`${local.preview.dimension.width} x ${local.preview.dimension.height}`}</span>{" "}
</p>
<p className="text-sm px-2">
File Size:{" "}
<span>{getSize(local.preview.details.size)}</span>
</p>
<p className="text-sm px-2">
Last Modified:{" "}
<span>
{format(
new Date(local.preview.details.detail.mtime),
"d MMMM yyyy"
)}
</span>{" "}
</p>
</div>
</>
) : (
<></>
)}
</div>
</>
) : (
<>
<div className="flex w-full h-full items-center justify-center">
<Loading note="gallery" backdrop={false} />
</div>
</>
)}
</>
);
};

BIN
bun.lockb

Binary file not shown.

View File

@ -19,5 +19,5 @@
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": {} "dependencies": { "@node-rs/argon2": "^1.5.2" }
} }

View File

@ -165,34 +165,25 @@ export const createFrameCors = async (url: string, win?: any) => {
}; };
export const fetchSendApi = async ( export const fetchSendApi = async (
_url: string, rawUrl: string,
params: any, params: any,
parentWindow?: any parentWindow?: any
) => { ) => {
let w: any = typeof window === "object" ? window : globalThis; let w: any = typeof window === "object" ? window : globalThis;
const win = parentWindow || w; const win = parentWindow || w;
let url = _url; const url = new URL(rawUrl);
let frm: Awaited<ReturnType<typeof createFrameCors>>; let frm: Awaited<ReturnType<typeof createFrameCors>>;
const base = `${url.protocol}//${url.host}`;
if (!win.frmapi) { if (!win.frmapi) {
win.frmapi = {}; win.frmapi = {};
win.frmapi[base] = await createFrameCors(base, win);
win.frmapi[w.serverurl] = await createFrameCors(w.serverurl, win);
} }
frm = win.frmapi[w.serverurl]; frm = win.frmapi[base];
if (url.startsWith("http")) {
const purl = new URL(url);
if (!win.frmapi[purl.host]) {
win.frmapi[purl.host] = await createFrameCors(
`${purl.protocol}//${purl.host}`
);
}
frm = win.frmapi[purl.host];
url = url.substring(`${purl.protocol}//${purl.host}`.length);
}
if (!win.apiHeaders) { if (!win.apiHeaders) {
win.apiHeaders = {}; win.apiHeaders = {};
} }
@ -204,5 +195,5 @@ export const fetchSendApi = async (
}); });
} }
return await frm.send(url, params, win.apiHeaders); return await frm.send(url.pathname, params, win.apiHeaders);
}; };