Skip to content

Commit

Permalink
Merge pull request #263 from mfts/feat/avatar
Browse files Browse the repository at this point in the history
feat: add avatar for visitors
  • Loading branch information
mfts authored Jan 29, 2024
2 parents f3a82b1 + 4c5d6e7 commit 9aec523
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 42 deletions.
14 changes: 3 additions & 11 deletions components/links/links-visitors.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { durationFormat, timeAgo } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { TableCell, TableRow } from "@/components/ui/table";
import { useLinkVisits } from "@/lib/swr/use-link";
import { Gauge } from "@/components/ui/gauge";
import { VisitorAvatar } from "@/components/visitors/visitor-avatar";

export default function LinksVisitors({
linkId,
Expand All @@ -23,11 +23,7 @@ export default function LinksVisitors({
<div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3 overflow-visible w-[220px]">
<Avatar className="flex-shrink-0 hidden sm:inline-flex">
<AvatarFallback>
{view.viewerEmail?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<VisitorAvatar viewerEmail={view.viewerEmail} />
<div className="min-w-0 flex-1">
<div className="focus:outline-none">
<p className="text-sm text-gray-800 dark:text-gray-200 overflow-visible">
Expand Down Expand Up @@ -60,11 +56,7 @@ export default function LinksVisitors({
<div>
<div className="flex items-center justify-between">
<div className="flex items-center truncate w-[220px]">
<Avatar className="flex-shrink-0 hidden sm:inline-flex">
<AvatarFallback>
{view.viewerEmail?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<VisitorAvatar viewerEmail={view.viewerEmail} />
<div className="min-w-0 flex-1">
<div className="focus:outline-none">
<p className="text-sm font-medium text-muted-foreground overflow-visible">
Expand Down
24 changes: 12 additions & 12 deletions components/ui/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";

import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";

const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
Expand All @@ -11,12 +11,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;

const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
Expand All @@ -27,8 +27,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;

const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
Expand All @@ -38,11 +38,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-gray-300 dark:bg-muted",
className
className,
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };
6 changes: 0 additions & 6 deletions components/view/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { Avatar, AvatarFallback } from "../ui/avatar";
import UserRound from "../shared/icons/user-round";
import { REACTIONS } from "@/lib/constants";
import GripVertical from "../shared/icons/grip-vertical";
import Draggable from "react-draggable";

function getKeyByValue(object: { [x: string]: any }, value: any) {
return Object.keys(object).find((key) => object[key] === value);
}

export default function Toolbar({
viewId,
pageNumber,
Expand Down
64 changes: 64 additions & 0 deletions components/visitors/visitor-avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { generateGravatarHash } from "@/lib/utils";

export const VisitorAvatar = ({
viewerEmail,
}: {
viewerEmail: string | null;
}) => {
// Convert email string to a simple hash
const hashString = (str: string) => {
let hash = 0;

for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Convert to 32bit integer
}
return hash;
};

// Get the background color from the email number hash
const getColorFromHash = (hash: number): string => {
// An array of colors you want to choose from
const colors = [
"bg-gray-200/50",
"bg-gray-300/50",
"bg-gray-400/50",
"bg-gray-500/50",
"bg-gray-600/50",
];

// Use the hash to get an index for the colors array
const index = Math.abs(hash) % colors.length;
return colors[index];
};

if (!viewerEmail) {
return (
<Avatar className="flex-shrink-0 hidden sm:inline-flex">
<AvatarFallback className="bg-gray-200/50 dark:bg-gray-200/50">
AN
</AvatarFallback>
</Avatar>
);
}

return (
<Avatar className="flex-shrink-0 hidden sm:inline-flex">
<AvatarImage
src={`https://gravatar.com/avatar/${generateGravatarHash(
viewerEmail,
)}?s=80&d=404`}
/>

<AvatarFallback
className={`${getColorFromHash(
hashString(viewerEmail),
)} dark:${getColorFromHash(hashString(viewerEmail))}`}
>
{viewerEmail?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
);
};
12 changes: 4 additions & 8 deletions components/visitors/visitors-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Gauge } from "@/components/ui/gauge";

import { useDocumentVisits } from "@/lib/swr/use-document";
import { durationFormat, timeAgo } from "@/lib/utils";
import { Skeleton } from "../ui/skeleton";
import ChevronDown from "../shared/icons/chevron-down";
import { Skeleton } from "@/components/ui/skeleton";
import ChevronDown from "@/components/shared/icons/chevron-down";
import VisitorChart from "./visitor-chart";
import { VisitorAvatar } from "./visitor-avatar";

export default function VisitorsTable({ numPages }: { numPages: number }) {
const { views } = useDocumentVisits();
Expand Down Expand Up @@ -48,11 +48,7 @@ export default function VisitorsTable({ numPages }: { numPages: number }) {
{/* Name */}
<TableCell className="">
<div className="flex items-center sm:space-x-3 overflow-visible">
<Avatar className="flex-shrink-0 hidden sm:inline-flex">
<AvatarFallback>
{view.viewerEmail?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<VisitorAvatar viewerEmail={view.viewerEmail} />
<div className="min-w-0 flex-1">
<div className="focus:outline-none">
<p className="text-sm font-medium text-gray-800 dark:text-gray-200 overflow-visible">
Expand Down
20 changes: 20 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { customAlphabet } from "nanoid";
import { ThreadMessage } from "openai/resources/beta/threads/messages/messages";
import { Message } from "ai";
import { upload } from "@vercel/blob/client";
import crypto from "crypto";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
Expand Down Expand Up @@ -383,3 +384,22 @@ export const uploadImage = async (file: File) => {

return newBlob.url;
};

/**
* Generates a Gravatar hash for the given email.
* @param {string} email - The email address.
* @returns {string} The Gravatar hash.
*/
export const generateGravatarHash = (email: string | null): string => {
if (!email) return "";
// 1. Trim leading and trailing whitespace from an email address
const trimmedEmail = email.trim();

// 2. Force all characters to lower-case
const lowerCaseEmail = trimmedEmail.toLowerCase();

// 3. Hash the final string with SHA256
const hash = crypto.createHash("sha256").update(lowerCaseEmail).digest("hex");

return hash;
};
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"framer-motion": "^10.16.14",
"fuse.js": "^6.6.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.292.0",
"lucide-react": "^0.316.0",
"ms": "^2.1.3",
"mupdf": "^0.1.1",
"nanoid": "^5.0.4",
Expand Down