Add Next SEO and clean some code

This commit is contained in:
mbaharip
2023-04-27 15:02:13 +07:00
parent 40712d9f3c
commit 3868e2e789
23 changed files with 118 additions and 1297 deletions
+7
View File
@@ -0,0 +1,7 @@
# You can generate encryption key from setup page.
ENCRYPTION_KEY=""
# You can use any random string for JWT_KEY, it is recommended to use a strong key by combining characters, numbers and symbols.
JWT_KEY=""
# Domain name of your website. Used for fetching the og-image from public directory.
# Example: https://drive.example.com
NEXT_PUBLIC_DOMAIN=""
+2 -4
View File
@@ -25,8 +25,8 @@ yarn-debug.log*
yarn-error.log*
# local env file
.env*
.env*.local
.env
.env.local
# vercel
.vercel
@@ -34,10 +34,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
!/src/pages/api/test.ts
# personal docs
/docs
/.next
/.vscode
/.idea
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>
+5 -6
View File
@@ -13,7 +13,6 @@ type Props = {
export default function Breadcrumb({ data, isLoading }: Props) {
const limitItem = 2;
const [limitedPath, setLimitedPath] = useState<TFileParent[]>([]);
const [slicedPath, setSlicedPath] = useState<TFileParent>();
const [isLimited, setIsLimited] = useState<boolean>();
// const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -21,12 +20,12 @@ export default function Breadcrumb({ data, isLoading }: Props) {
// setIsLoading(true);
if (data.length > 0) {
const findRoot = data.find((item) => item.id === config.files.rootFolder);
let _data = data;
if (findRoot) {
data = data.filter((item) => item.id !== config.files.rootFolder);
_data = _data.filter((item) => item.id !== config.files.rootFolder);
}
setLimitedPath(data.slice(0, limitItem).reverse());
setSlicedPath(data.slice(limitItem)[0]);
setIsLimited(data.length > limitItem);
setLimitedPath(_data.slice(0, limitItem).reverse());
setIsLimited(_data.length > limitItem);
// setIsLoading(false);
}
}, [data]);
@@ -61,7 +60,7 @@ export default function Breadcrumb({ data, isLoading }: Props) {
<span className={"cursor-default"}>/</span>
{idx === limitedPath.length - 1 ? (
<span className='flex cursor-default cursor-default items-center gap-2 font-bold'>
<span className='flex cursor-default items-center gap-2 font-bold'>
{parent.name}
</span>
) : (
+1 -1
View File
@@ -37,7 +37,7 @@ export default function GridFile({ data }: Props) {
{data.videoMediaMetadata && (
<>
<MdPlayCircleFilled className='center absolute h-8 w-8 text-white/50 transition-colors duration-300 group-hover:text-white/80' />
<span className='absolute bottom-1 right-1 rounded-lg bg-zinc-950/50 px-1 py-0.5 text-xs text-white'>
<span className='absolute bottom-0 right-0 rounded-lg rounded-bl-none rounded-tr-none bg-zinc-950/75 px-1 py-0.5 text-xs text-white'>
{formatDuration(data.videoMediaMetadata.durationMillis!)}
</span>
</>
@@ -70,7 +70,7 @@ export default function DetailsButtons({ data, hash }: Props) {
<MdCopyAll />
Copy direct link
</button>
{!config.files.allowDownloadProtectedWithoutAccess && (
{hash && !config.files.allowDownloadProtectedWithoutAccess && (
<div className={"banner warning text-sm"}>
<div className={"flex flex-col gap-2"}>
<div className={"font-bold"}>
+23 -11
View File
@@ -1,13 +1,10 @@
import { ErrorResponse, FileResponse, TFile } from "@/types/googleapis";
import { TFile } from "@/types/googleapis";
import { drive_v3 } from "googleapis";
import LoadingFeedback from "@components/APIFeedback/Loading";
import { formatBytes, formatDate, formatDuration } from "@utils/formatHelper";
import { useEffect, useState } from "react";
import DetailsButtons from "@components/layout/FileDetails/DetailsButtons";
import ImagePreview from "@components/FilePreview/ImagePreview";
import MarkdownRender from "@components/utility/MarkdownRender";
import fetcher from "@utils/swrFetch";
import useSWR from "swr";
import { getFilePreview } from "@utils/mimeTypesHelper";
type Props = {
data: TFile | drive_v3.Schema$File;
@@ -18,6 +15,20 @@ export default function FileDetails({ data, hash }: Props) {
const [metadata, setMetadata] = useState<{ label: string; value: string }[]>(
[],
);
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element | null>(
null,
);
useEffect(() => {
const Preview = getFilePreview(data.fileExtension as string);
setPreviewComponent(
<Preview
data={data}
hash={hash || ""}
/>,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (data) {
@@ -73,12 +84,13 @@ export default function FileDetails({ data, hash }: Props) {
<div className={"divider-horizontal"} />
{data.mimeType?.startsWith("image") && (
<ImagePreview
data={data}
hash={hash || ""}
/>
)}
{PreviewComponent}
{/*{data.mimeType?.startsWith("image") && (*/}
{/* <ImagePreview*/}
{/* data={data}*/}
{/* hash={hash || ""}*/}
{/* />*/}
{/*)}*/}
{/*{data.mimeType?.startsWith("audio") && (*/}
<div className='flex w-full items-center justify-center'>
{/*<video*/}
+1 -1
View File
@@ -84,7 +84,7 @@ export default function GridLayout({ data, pagination }: Props) {
<button
disabled={isLoadingMore}
onClick={() => {
setSize(size + 1);
setSize(size + 1).then((r) => r);
}}
className='w-full rounded-lg'
>
+1 -1
View File
@@ -59,7 +59,7 @@ export default function ListLayout({ data, pagination }: Props) {
<button
disabled={isLoadingMore}
onClick={() => {
setSize(size + 1);
setSize(size + 1).then((r) => r);
}}
className='w-full rounded-lg'
>
+9
View File
@@ -55,6 +55,15 @@ export default function Navbar() {
const { data, error, isLoading } = useSWR<SearchResponse, ErrorResponse>(
`/api/search?query=${debouncedSearchQuery}`,
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
},
);
useEffect(() => {
+3 -7
View File
@@ -1,21 +1,17 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useState } from "react";
import useLocalStorage from "@hooks/useLocalStorage";
import { IoMdEye, IoMdEyeOff } from "react-icons/io";
import { MdLock } from "react-icons/md";
import { useRouter } from "next/router";
import { hashToken } from "@utils/hashHelper";
import ReactLoading from "react-loading";
type Props = {
folderId: string;
inputCallback: (data: { [p: string]: string }) => void;
};
export default function Password({ folderId, inputCallback }: Props) {
const router = useRouter();
const [password, setPassword] = useState<string>("");
const [showPassword, setShowPassword] = useState<boolean>(false);
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
const [passwordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
@@ -63,7 +59,7 @@ export default function Password({ folderId, inputCallback }: Props) {
onChange={(e) => {
setPassword(e.target.value);
}}
onKeyPress={(e) => {
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit();
}
+3
View File
@@ -4,6 +4,9 @@ const config = {
/* Site MetaData */
// The name of the site
siteName: "My Drive Linker",
// The description of the site
// Used in meta tags and document head
siteDescription: "My Drive Linker",
// Fav icon of the site
// Also used as the logo of the site on the navbar
siteIcon: "/favicon.ico",
+31 -1
View File
@@ -1,5 +1,4 @@
import type { AppProps } from "next/app";
import { SWRConfig } from "swr";
import Navbar from "@/components/layout/Navbar";
import Footer from "@/components/layout/Footer";
import NextNProgress from "nextjs-progressbar";
@@ -14,6 +13,8 @@ import { Exo_2, Source_Sans_Pro, JetBrains_Mono } from "next/font/google";
import { IconContext } from "react-icons";
import { ToastContainer } from "react-toastify";
import useLocalStorage from "@hooks/useLocalStorage";
import { DefaultSeo, DefaultSeoProps } from "next-seo";
import config from "@config/site.config";
const exo2 = Exo_2({
weight: ["300", "400", "600", "700"],
@@ -39,10 +40,39 @@ const jetBrainsMono = JetBrains_Mono({
export default function App({ Component, pageProps }: AppProps) {
const [isDarkMode] = useLocalStorage<boolean>("isDarkMode", false);
const SEOConfig: DefaultSeoProps = {
titleTemplate: `%s | ${config.siteName}`,
defaultTitle: config.siteName,
description: config.siteDescription,
dangerouslySetAllPagesToNoFollow: true,
dangerouslySetAllPagesToNoIndex: true,
openGraph: {
type: "website",
title: config.siteName,
description: config.siteDescription,
images: [
{
url: `${process.env.NEXT_PUBLIC_DOMAIN}/og-image.png`,
width: 1200,
height: 630,
alt: config.siteName,
},
],
siteName: config.siteName,
},
twitter: {
handle: "@mbaharip_",
site: "@mbaharip_",
cardType: "summary_large_image",
},
};
return (
<main
className={`${exo2.variable} ${sourceSansPro.variable} ${jetBrainsMono.variable} font-body`}
>
<DefaultSeo {...SEOConfig} />
<IconContext.Provider
value={{
size: "18px",
-3
View File
@@ -4,8 +4,6 @@ import { NextApiRequest, NextApiResponse } from "next";
import config from "@config/site.config";
import { validateProtected } from "@utils/driveHelper";
import { ExtendedError } from "@/types/default";
import { decrypt } from "@utils/encryptionHelper";
import { verify } from "jsonwebtoken";
import { reverseString } from "@utils/hashHelper";
export default async function handler(
@@ -14,7 +12,6 @@ export default async function handler(
) {
try {
const { id, hash } = request.query;
const { vector, data } = request.query;
const { authorization } = request.headers;
const headerHash = authorization?.split(" ")[1] || null;
-18
View File
@@ -1,18 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import drive from "@utils/driveClient";
import { hashToken } from "@utils/hashHelper";
export default async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
const files = await drive.files.list({
q: "'1VMU0sQOkuI06icRRJFof-6V-NLyBWlp5' in parents",
fields: "files(id, name, mimeType, size)",
});
const { password } = request.query;
return response.status(200).json({
hash: hashToken(password as string),
file: files.data.files,
});
}
+11 -6
View File
@@ -10,11 +10,13 @@ import useLocalStorage from "@hooks/useLocalStorage";
import axios from "axios";
import { GetServerSidePropsContext } from "next";
import Password from "@components/layout/Password";
import { NextSeo } from "next-seo";
type Props = {
passwordParent?: string;
fileName?: string;
};
export default function File({ passwordParent }: Props) {
export default function File({ passwordParent, fileName }: Props) {
const router = useRouter();
const { id } = router.query;
@@ -93,6 +95,8 @@ export default function File({ passwordParent }: Props) {
useEffect(() => {
mutate(swrData, {
revalidate: true,
}).then((r) => {
return r;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [password]);
@@ -108,6 +112,7 @@ export default function File({ passwordParent }: Props) {
return (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<NextSeo title={fileName || "File preview"} />
{globalLoading && <LoadingFeedback message={"Loading file details..."} />}
{!globalLoading && error && (
<ErrorFeedback message={error.errors?.message} />
@@ -143,25 +148,25 @@ export default function File({ passwordParent }: Props) {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { id } = context.query;
const passwordParent = await axios.get(
`http://localhost:5000/api/files/${id}`,
);
const data = await axios.get(`http://localhost:5000/api/files/${id}`);
context.res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59",
);
if (passwordParent) {
if (data) {
return {
props: {
passwordParent: passwordParent.data.protectedId || null,
passwordParent: data.data.protectedId || null,
fileName: data.data.file.name,
},
};
} else {
return {
props: {
passwordParent: "",
fileName: "",
},
};
}
+12 -14
View File
@@ -1,16 +1,10 @@
import useSWR, { mutate } from "swr";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
import fetcher, { buildNextKey } from "@utils/swrFetch";
import { ErrorResponse, FilesResponse, TFile } from "@/types/googleapis";
import Breadcrumb from "@/components/Breadcrumb";
import { drive_v3 } from "googleapis";
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from "react";
import { useCallback, useEffect, useState } from "react";
import MarkdownRender from "@/components/utility/MarkdownRender";
import config from "@config/site.config";
import GridLayout from "@components/layout/Files/GridLayout";
@@ -23,11 +17,13 @@ import { useRouter } from "next/router";
import axios from "axios";
import Password from "@components/layout/Password";
import { GetServerSidePropsContext } from "next";
import { NextSeo } from "next-seo";
type Props = {
passwordParent?: string;
folderName?: string;
};
export default function Folder({ passwordParent }: Props) {
export default function Folder({ passwordParent, folderName }: Props) {
const router = useRouter();
const { id } = router.query;
@@ -150,6 +146,8 @@ export default function Folder({ passwordParent }: Props) {
return (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<NextSeo title={folderName || "Folder"} />
<div className='flex items-center justify-between'>
<Breadcrumb
data={data?.parents || []}
@@ -236,25 +234,25 @@ export default function Folder({ passwordParent }: Props) {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { id } = context.query;
const passwordParent = await axios.get(
`http://localhost:5000/api/files/${id}`,
);
const data = await axios.get(`http://localhost:5000/api/files/${id}`);
context.res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59",
);
if (passwordParent) {
if (data) {
return {
props: {
passwordParent: passwordParent.data.protectedId || null,
passwordParent: data.data.protectedId || null,
folderName: data.data.parents?.[0]?.name || null,
},
};
} else {
return {
props: {
passwordParent: "",
folderName: "",
},
};
}
File diff suppressed because it is too large Load Diff
-9
View File
@@ -94,15 +94,6 @@ export interface SearchResponse extends APIResponse {
files: (TFile | drive_v3.Schema$File)[];
}
export interface ReadmeResponse extends APIResponse {
file: TFile | drive_v3.Schema$File;
}
export interface PasswordResponse extends APIResponse {
passwordRequired: boolean;
passwordValidated: boolean;
}
export interface ErrorResponse extends APIResponse {
code: number;
errors: {
-5
View File
@@ -9,8 +9,3 @@ export interface ExpiredJWTPayload {
exp: number;
isExpired: boolean;
}
export interface ProtectionPayload {
fileId: string;
password: string;
}
-38
View File
@@ -33,44 +33,6 @@ export function buildQuery({
return query.join(" and ");
}
export async function _checkProtected(id: string) {
try {
const files = await drive.files.list({
q: id ? buildQuery({ id }) : buildQuery({}),
fields: "files(id)",
});
return {
protected: files.data.files?.length,
id: files.data.files?.[0].id || null,
};
} catch (error: any) {
return {
protected: false,
id: null,
};
}
}
export async function _validateFolderPassword(
passwordFileId: string,
password: string,
) {
try {
const folderPassword = await drive.files.get(
{
fileId: passwordFileId,
alt: "media",
},
{ responseType: "text" },
);
return folderPassword.data === password;
} catch (error: any) {
return false;
}
}
export async function validateProtected(
fileId: string | TFileParent[],
passwordHash?: string,
+1 -5
View File
@@ -4,7 +4,6 @@ import {
BsBoxFill,
BsDatabaseFill,
BsFileEarmarkBinaryFill,
BsFileEarmarkBreakFill,
BsFileEarmarkCodeFill,
BsFileEarmarkFill,
BsFileEarmarkFontFill,
@@ -13,14 +12,11 @@ import {
BsFileEarmarkPdfFill,
BsFileEarmarkPlayFill,
BsFileEarmarkRichtextFill,
BsFileEarmarkRuledFill,
BsFileEarmarkSlidesFill,
BsFileEarmarkSpreadsheetFill,
BsFileEarmarkTextFill,
BsFileEarmarkWordFill,
BsFileEarmarkZipFill,
BsFillDatabaseFill,
BsFolderFill,
} from "react-icons/bs";
import ModelPreview from "@components/FilePreview/ModelPreview";
import AudioPreview from "@components/FilePreview/AudioPreview";
@@ -33,7 +29,7 @@ import CodePreview from "@components/FilePreview/CodePreview";
import TextPreview from "@components/FilePreview/TextPreview";
import VideoPreview from "@components/FilePreview/VideoPreview";
export default function findMimeType(extension: string): string {
function findMimeType(extension: string): string {
return mime.lookup(extension) || "application/octet-stream";
}
-11
View File
@@ -4,17 +4,6 @@ import { FilesResponse } from "@/types/googleapis";
const fetcher = async <T>(url: string, headers: Record<string, string> = {}) =>
axios.get<T>(url, { headers }).then((res) => res.data);
function getNextKey(
pageIndex: number,
previousPageData: FilesResponse,
): string | null {
if (previousPageData && !previousPageData.nextPageToken) {
return null;
}
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
return `/api/files?pageToken=${pageToken}`;
}
export function buildNextKey(apiURL: string) {
return (pageIndex: number, previousPageData: FilesResponse) => {
if (previousPageData && !previousPageData.nextPageToken) {