Skip to content

Commit

Permalink
added a view-for-all page (#102)
Browse files Browse the repository at this point in the history
* added a view-for-all page

* added copied to clipboard toast
  • Loading branch information
e-for-eshaan authored Dec 12, 2023
1 parent 82450fe commit 8105210
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 48 deletions.
50 changes: 32 additions & 18 deletions src/components/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { GiPaperClip, GiShare } from 'react-icons/gi';
import {} from 'react-icons';
import { FaTwitter, FaDownload } from 'react-icons/fa';
import toast from 'react-hot-toast';
import { track } from '@/constants/events';
import toast from 'react-hot-toast';
import { logException } from '@/utils/logger';

type ShareButtonProps = {
Expand Down Expand Up @@ -100,27 +100,19 @@ export const ShareButton: React.FC<ShareButtonProps> = ({
const CopyPaperClip: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
const [isCopied, setIsCopied] = useState(false);

const copyToClipboard = async () => {
if (isCopied) return;
try {
await navigator.clipboard.writeText(textToCopy);
// toast
toast.success('Copied to clipboard!', {
position: 'top-right'
});
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
track('SINGLE_IMAGE_PUBLIC_LINK_COPIED');
} catch (err) {
logException('Error copying to clipboard', { originalException: err });
}
const onCopy = () => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
track('SINGLE_IMAGE_PUBLIC_LINK_COPIED');
};

return (
<div
onClick={copyToClipboard}
onClick={() => {
copyToClipboard(textToCopy, onCopy);
}}
className="absolute flex items-center justify-center rounded-full -top-[6px] -left-14 w-10 h-10 bg-white"
>
<GiPaperClip
Expand All @@ -138,3 +130,25 @@ const CopyPaperClip: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
</div>
);
};

export const copyToClipboard = async (
textToCopy: string,
onCopy?: (e: any) => void,
onFailure?: (e: any) => void
) => {
try {
await navigator.clipboard.writeText(textToCopy);
toast.success('Copied to clipboard', {
position: 'top-right'
});
onCopy && onCopy(textToCopy);
} catch (err) {
toast.error('Error copying to clipboard', {
position: 'top-right'
});
logException(`Error copying to clipboard: ${textToCopy}`, {
originalException: err
});
onFailure && onFailure(err);
}
};
33 changes: 21 additions & 12 deletions src/components/SwiperCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ interface SwiperCarouselProps {
}: {
images: UpdatedImageFile;
}) => void;
useLinksToRenderImages?: boolean;
hideShareButtons?: boolean;
}

const SwiperCarousel: React.FC<SwiperCarouselProps> = ({
singleImageSharingCallback,
userName,
images
images,
useLinksToRenderImages = false,
hideShareButtons = false
}) => {
const sliderRef = useRef<SwiperRef | null>(null);

Expand Down Expand Up @@ -90,24 +94,29 @@ const SwiperCarousel: React.FC<SwiperCarouselProps> = ({
>
{images.toSorted(sortImageCards).map((image, index) => (
<SwiperSlide key={index} className="swiper-slide-img">
<ShareButton
userName={userName}
imageName={extractFilenameWithoutExtension(image.fileName)}
imageUrl={image.url}
className="share-active-image cursor-pointer"
callBack={() => {
singleImageSharingCallback({ images: image });
track('SINGLE_IMAGE_SHARE_CLICKED');
}}
/>
{!hideShareButtons && (
<ShareButton
userName={userName}
imageName={extractFilenameWithoutExtension(image.fileName)}
imageUrl={image.url}
className="share-active-image cursor-pointer"
callBack={() => {
singleImageSharingCallback({ images: image });
track('SINGLE_IMAGE_SHARE_CLICKED');
}}
/>
)}
{index !== 0 && (
<IoIosArrowDropleftCircle
size={36}
className="prev-arrow right-[90%] sm:right-[102%]"
onClick={handlePrev}
/>
)}
<img src={image.data} alt={`Slide ${index + 1}`} />
<img
src={useLinksToRenderImages ? image.url : image.data}
alt={`Slide ${index + 1}`}
/>

<IoIosArrowDroprightCircle
size={36}
Expand Down
32 changes: 19 additions & 13 deletions src/pages/api/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
extractFilenameWithoutExtension
} from '@/utils/stringHelpers';
import { logException } from '@/utils/logger';
import { ImageAPIResponse } from '@/types/images';

const fetchAndDownloadImageBuffer = async (
req: NextApiRequest,
Expand Down Expand Up @@ -63,20 +64,25 @@ const fetchAndDownloadImageBuffer = async (
res.setHeader('Content-Type', 'application/zip');
res.send(zippedData);
} else {
res.setHeader('Content-Type', 'application/json');
res.send(
imageBuffer.map(({ data, fileName }) => {
const file = extractFilenameWithoutExtension(fileName);
const username = user.login;
const hash = bcryptGen(username + file);
const username = user.login;
const userNameHash = bcryptGen(username);
const shareUrl = `/view/${user.login}/${userNameHash}`;

return {
fileName,
url: `/shared/${username}/${file}/${hash}`,
data: `data:image/png;base64,${data.toString('base64')}`
};
})
);
const imageData = imageBuffer.map(({ data, fileName }) => {
const file = extractFilenameWithoutExtension(fileName);
const hash = bcryptGen(username + file);

return {
fileName,
url: `/shared/${username}/${file}/${hash}`,
data: `data:image/png;base64,${data.toString('base64')}`
};
});
res.setHeader('Content-Type', 'application/json');
res.send({
shareAllUrl: shareUrl,
data: imageData
} as ImageAPIResponse);
}
console.log(chalk.green('Successfully sent buffer to client'));
} catch (error: any) {
Expand Down
44 changes: 44 additions & 0 deletions src/pages/api/getAllImages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// api to check if hash is valid, accepts username and hash, return true or false

import {
bcryptGen,
extractFilenameWithoutExtension
} from '@/utils/stringHelpers';
import { NextApiRequest, NextApiResponse } from 'next';
import { fetchSavedCards } from '@/api-helpers/persistance';
import { logException } from '@/utils/logger';

const checkHash = async (req: NextApiRequest, res: NextApiResponse) => {
const { username, hash } = req.query;
if (!username || !hash) {
return res.status(400).json({
message: 'Username or hash not found.'
});
}

const userNameHash = bcryptGen(username as string);
const isValid = userNameHash === hash;

if (isValid) {
try {
const imageData = await fetchSavedCards(username as string);
const imageDataWithURL = imageData.map((image) => {
const filename = extractFilenameWithoutExtension(image.fileName);
const hash = bcryptGen((username as string) + filename);
return {
...image,
url: `/shared/${username}/${filename}/${hash}`
};
});

res.status(200).json({ isValid, data: imageDataWithURL });
} catch (e) {
logException('Error fetching from share-all data from s3', {
originalException: e
});
res.status(400).json({ isValid, data: null });
}
} else res.status(400).json({ isValid, data: null });
};

export default checkHash;
21 changes: 16 additions & 5 deletions src/pages/stats-unwrapped.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { handleRequest } from '@/utils/axios';
import { LoaderWithFacts } from '@/components/LoaderWithFacts';
import SwiperCarousel from '@/components/SwiperCarousel';
import { FaDownload } from 'react-icons/fa';
import { FaFilePdf } from 'react-icons/fa6';
import { FaFilePdf, FaShare } from 'react-icons/fa6';
import { FaLinkedin } from 'react-icons/fa';
import { useImageDownloader } from '@/hooks/useImageDownloader';
import { useImageDownloaderAsPdf } from '@/hooks/useImageDownloaderAsPdfHook';
import Confetti from 'react-confetti';
import Link from 'next/link';
import toast from 'react-hot-toast';
import { UpdatedImageFile } from '@/types/images';
import { ImageAPIResponse, UpdatedImageFile } from '@/types/images';
import { track } from '@/constants/events';
import { GithubUser } from '@/api-helpers/exapi-sdk/types';
import { copyToClipboard } from '@/components/ShareButton';
import { Tooltip } from 'react-tooltip';

const LINKEDIN_URL = 'https://www.linkedin.com/';
Expand All @@ -25,12 +26,13 @@ export default function StatsUnwrapped() {
null
);
const [userName, setUserName] = useState('');

const [shareUrl, setShareUrl] = useState('');
useEffect(() => {
setIsLoading(true);
handleRequest<UpdatedImageFile[]>('/api/download')
handleRequest<ImageAPIResponse>('/api/download')
.then((res) => {
setUnwrappedImages(res);
setUnwrappedImages(res.data);
setShareUrl(res.shareAllUrl);
})
.catch((_) => {
toast.error('Something went wrong', {
Expand Down Expand Up @@ -70,6 +72,7 @@ export default function StatsUnwrapped() {
{images?.length && (
<div className="flex flex-col items-center gap-4 w-full ">
<SwiperCarousel
useLinksToRenderImages
userName={userName}
images={images}
singleImageSharingCallback={downloadImage}
Expand Down Expand Up @@ -105,6 +108,14 @@ export default function StatsUnwrapped() {
data-tooltip-id="carousel-action-menu"
data-tooltip-content="Download as PDF"
/>
{shareUrl && (
<FaShare
size={36}
onClick={() => {
copyToClipboard(window.location.origin + shareUrl);
}}
/>
)}
</div>
<Tooltip id="carousel-action-menu" />
</div>
Expand Down
86 changes: 86 additions & 0 deletions src/pages/view/[username]/[...hash].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
import { handleRequest } from '@/utils/axios';
import { LoaderWithFacts } from '@/components/LoaderWithFacts';
import SwiperCarousel from '@/components/SwiperCarousel';
import { useImageDownloader } from '@/hooks/useImageDownloader';
import Confetti from 'react-confetti';
import toast from 'react-hot-toast';
import { UpdatedImageFile } from '@/types/images';
import { useRouter } from 'next/router';

export default function StatsUnwrapped() {
const [isLoading, setIsLoading] = useState(false);
const downloadImage = useImageDownloader();

const router = useRouter();
const userName = router.query.username as string;
const hash = (router.query.hash as string[])?.join('/');

const [isUrlValid, setIsUrlValid] = useState(false);
const [images, setImages] = useState<UpdatedImageFile[] | null>(null);

useEffect(() => {
if (!userName || !hash || isUrlValid) return;
setIsLoading(true);
handleRequest<{
isValid: boolean;
data: UpdatedImageFile[];
}>('/api/getAllImages', {
method: 'GET',
params: {
hash: hash,
username: userName
}
})
.then((res) => {
setIsUrlValid(res.isValid);
if (res.isValid) {
const imageData: UpdatedImageFile[] = res.data.map((link) => ({
url: `${window.location.origin}${link.url}`,
fileName: link.fileName,
data: link.data
}));
setImages(imageData);
}
})
.catch((_) => {
toast.error('Invalid URL', {
position: 'top-right'
});
})
.finally(() => {
setIsLoading(false);
});
}, [userName, hash, isUrlValid]);

if (isLoading) {
return (
<div className="h-screen flex flex-col items-center justify-between p-10">
<LoaderWithFacts />
</div>
);
}
return (
<div className="items-center justify-center p-4 min-h-screen w-full flex flex-col gap-10 text-center">
<div>
<h2 className="text-2xl">
Unwrap{' '}
<span className="font-bold text-[#bc9aef]">{userName}&apos;s</span>{' '}
GitHub journey of 2023! 🎉
</h2>
</div>
{images?.length && <Confetti numberOfPieces={400} recycle={false} />}
{images?.length && (
<div className="flex flex-col items-center gap-4 w-full ">
<SwiperCarousel
useLinksToRenderImages
hideShareButtons
userName={userName}
images={images}
singleImageSharingCallback={downloadImage}
/>
</div>
)}
</div>
);
}
5 changes: 5 additions & 0 deletions src/types/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export type UpdatedImageFile = {
data: string;
url: string;
};

export type ImageAPIResponse = {
data: UpdatedImageFile[];
shareAllUrl: string;
};

0 comments on commit 8105210

Please sign in to comment.