Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed app/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions components/links/link-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const DEFAULT_LINK_PROPS = (linkType: LinkType) => ({
metaTitle: null,
metaDescription: null,
metaImage: null,
metaFavicon: null,
enabledQuestion: false,
questionText: null,
questionType: null,
Expand Down Expand Up @@ -91,6 +92,7 @@ export type DEFAULT_LINK_TYPE = {
metaTitle: string | null; // metatags
metaDescription: string | null; // metatags
metaImage: string | null; // metatags
metaFavicon: string | null; // metaFavicon
enableQuestion?: boolean; // feedback question
questionText: string | null;
questionType: string | null;
Expand Down
192 changes: 186 additions & 6 deletions components/links/link-sheet/og-section.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useCallback, useEffect, useState } from "react";
import { ChangeEvent, useCallback, useEffect, useState } from "react";

import { useTeam } from "@/context/team-context";
import { LinkPreset } from "@prisma/client";
import { Label } from "@radix-ui/react-label";
import { motion } from "framer-motion";
import { Upload as ArrowUpTrayIcon } from "lucide-react";
import { Upload as ArrowUpTrayIcon, PlusIcon } from "lucide-react";
import useSWRImmutable from "swr/immutable";

import { Input } from "@/components/ui/input";
import LoadingSpinner from "@/components/ui/loading-spinner";
import { Textarea } from "@/components/ui/textarea";

import { FADE_IN_ANIMATION_SETTINGS } from "@/lib/constants";
import { cn, fetcher } from "@/lib/utils";
import { cn, fetcher, validateImageDimensions } from "@/lib/utils";
import { resizeImage } from "@/lib/utils/resize-image";

import { DEFAULT_LINK_TYPE } from ".";
Expand All @@ -35,7 +36,13 @@ export default function OGSection({
}: LinkUpgradeOptions) => void;
editLink: boolean;
}) {
const { enableCustomMetatag, metaTitle, metaDescription, metaImage } = data;
const {
enableCustomMetatag,
metaTitle,
metaDescription,
metaImage,
metaFavicon,
} = data;
const teamInfo = useTeam();
const { data: presets } = useSWRImmutable<LinkPreset>(
`/api/teams/${teamInfo?.currentTeam?.id}/presets`,
Expand All @@ -45,6 +52,8 @@ export default function OGSection({
const [enabled, setEnabled] = useState<boolean>(false);
const [fileError, setFileError] = useState<string | null>(null);
const [dragActive, setDragActive] = useState(false);
const [faviconFileError, setFaviconFileError] = useState<string | null>(null);
const [faviconDragActive, setFaviconDragActive] = useState(false);

const onChangePicture = useCallback(
async (e: any) => {
Expand Down Expand Up @@ -72,11 +81,15 @@ export default function OGSection({
}, [enableCustomMetatag]);

useEffect(() => {
if (presets && !(metaTitle || metaDescription || metaImage)) {
if (
presets &&
!(metaTitle || metaDescription || metaImage || metaFavicon)
) {
const preset = presets;
if (preset) {
setData((prev) => ({
...prev,
metaFavicon: prev.metaFavicon || preset.metaFavicon,
metaImage: prev.metaImage || preset.metaImage,
metaTitle: prev.metaTitle || preset.metaTitle,
metaDescription: prev.metaDescription || preset.metaDescription,
Expand All @@ -88,6 +101,7 @@ export default function OGSection({
presets,
setData,
editLink,
metaFavicon,
enableCustomMetatag,
metaTitle,
metaDescription,
Expand All @@ -110,6 +124,48 @@ export default function OGSection({
});
};

const onChangeFavicon = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
setFaviconFileError(null);
const file = e.target.files && e.target.files[0];
if (file) {
if (file.size / 1024 / 1024 > 1) {
setFaviconFileError("File size too big (max 1MB)");
} else if (
file.type !== "image/png" &&
file.type !== "image/x-icon" &&
file.type !== "image/svg+xml"
) {
setFaviconFileError(
"File type not supported (.png, .ico, .svg only)",
);
} else {
const image = await resizeImage(file, {
width: 36,
height: 36,
quality: 1,
});
const isValidDimensions = await validateImageDimensions(
image,
16,
48,
);
if (!isValidDimensions) {
setFaviconFileError(
"Image dimensions must be between 16x16 and 48x48",
);
} else {
setData((prev) => ({
...prev,
metaFavicon: image,
}));
}
}
}
},
[setData],
);

return (
<div className="pb-5">
<LinkItem
Expand Down Expand Up @@ -235,7 +291,131 @@ export default function OGSection({
/>
</div>
</div>

<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="faviconIcon">
<p className="block text-sm font-medium text-foreground">
Favicon Icon{" "}
<span className="text-sm italic text-muted-foreground">
(max 1 MB)
</span>
</p>
</Label>
{faviconFileError ? (
<p className="text-sm text-red-500">{faviconFileError}</p>
) : null}
</div>
<label
htmlFor="faviconIcon"
className="group relative mt-1 flex h-[4rem] w-[12rem] cursor-pointer flex-col items-center justify-center rounded-md border border-gray-300 bg-white shadow-sm transition-all hover:bg-gray-50"
style={{
backgroundImage:
"linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%)",
backgroundSize: "20px 20px",
backgroundPosition: "0 0, 10px 0, 10px -10px, 0px 10px",
}}
>
{false && (
<div className="absolute z-[5] flex h-full w-full items-center justify-center rounded-md bg-white">
<LoadingSpinner />
</div>
)}
<div
className="absolute z-[5] h-full w-full rounded-md"
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
setFaviconDragActive(true);
}}
onDragEnter={(e) => {
e.preventDefault();
e.stopPropagation();
setFaviconDragActive(true);
}}
onDragLeave={(e) => {
e.preventDefault();
e.stopPropagation();
setFaviconDragActive(false);
}}
onDrop={async (e) => {
e.preventDefault();
e.stopPropagation();
setFaviconDragActive(false);
setFaviconFileError(null);
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file) {
if (file.size / 1024 / 1024 > 1) {
setFaviconFileError("File size too big (max 1MB)");
} else if (
file.type !== "image/png" &&
file.type !== "image/x-icon" &&
file.type !== "image/svg+xml"
) {
setFaviconFileError(
"File type not supported (.png, .ico, .svg only)",
);
} else {
const image = await resizeImage(file, {
width: 36,
height: 36,
quality: 1,
});
const isValidDimensions = await validateImageDimensions(
image,
16,
48,
);
if (!isValidDimensions) {
setFaviconFileError(
"Image dimensions must be between 16x16 and 48x48",
);
} else {
setData((prev) => ({
...prev,
metaFavicon: image,
}));
}
}
}
}}
/>
<div
className={`${
faviconDragActive
? "cursor-copy border-2 border-black bg-gray-50 opacity-100"
: ""
} absolute z-[3] flex h-full w-full flex-col items-center justify-center rounded-md bg-white transition-all ${
metaFavicon
? "opacity-0 group-hover:opacity-100"
: "group-hover:bg-gray-50"
}`}
>
<PlusIcon
className={`${
faviconDragActive ? "scale-110" : "scale-100"
} h-7 w-7 text-gray-500 transition-all duration-75 group-hover:scale-110 group-active:scale-95`}
/>
<span className="sr-only">OG image upload</span>
</div>
{metaFavicon && (
<img
src={metaFavicon}
alt="Preview"
className="h-full w-full rounded-md object-contain"
/>
)}
</label>
<div className="mt-1 hidden rounded-md shadow-sm">
<input
id="faviconIcon"
name="favicon"
type="file"
accept="image/png,image/x-icon,image/svg+xml"
className="sr-only"
onChange={onChangeFavicon}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<p className="block text-sm font-medium text-foreground">Title</p>
Expand Down
1 change: 1 addition & 0 deletions components/links/links-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default function LinksTable({
metaTitle: link.metaTitle,
metaDescription: link.metaDescription,
metaImage: link.metaImage,
metaFavicon: link.metaFavicon,
enableAgreement: link.enableAgreement ? link.enableAgreement : false,
agreementId: link.agreementId,
showBanner: link.showBanner ?? false,
Expand Down
Loading