Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.

Commit

Permalink
feature: show profile age in stat table
Browse files Browse the repository at this point in the history
fix: userdata in githubcontext
  • Loading branch information
sametcn99 committed Mar 14, 2024
1 parent 3e7a35e commit 8c2b661
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 27 deletions.
1 change: 1 addition & 0 deletions app/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default async function fetchUserPage(searchParams: SearchParams) {
username={username}
repoCount={userData.public_repos}
gistCount={userData.public_gists}
user={userData}
>
<StatsProvider>
<TabWrapper />
Expand Down
12 changes: 5 additions & 7 deletions app/context/GithubContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ import { createContext, ReactNode, useState, useEffect } from "react";
export type GithubContextProps = {
repos: GitHubRepo[] | [];
gists: GitHubRepo[] | [];
user: UserData | null;
loading: boolean;
setUser: (user: UserData) => void; // Add this line
user: UserData | null;
};

// Create a context with the defined structure
export const GithubContext = createContext<GithubContextProps>({
repos: [],
gists: [],
user: null,
loading: false,
setUser: () => {},
user: null,
});

// Create a provider component for the GitHub context
Expand All @@ -25,16 +23,17 @@ export const GithubProvider = ({
username,
repoCount,
gistCount,
user,
}: {
children: ReactNode;
username: string;
repoCount: number;
gistCount: number;
user: UserData;
}) => {
const [repos, setRepos] = useState<GitHubRepo[] | []>([]);
const [gists, setGists] = useState<GitHubRepo[] | []>([]);
const [loading, setLoading] = useState<boolean>(true);
const [user, setUser] = useState<UserData | null>(null);

useEffect(() => {
const fetchData = async () => {
Expand Down Expand Up @@ -66,9 +65,8 @@ export const GithubProvider = ({
const contextValue = {
repos,
gists,
user,
setUser,
loading,
user,
};

// Provide the context value to the child components
Expand Down
9 changes: 0 additions & 9 deletions components/ProfileCard/ProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
"use client";
import { Card } from "@radix-ui/themes";
import ProfileCardHeader from "./ProfileCardHeader";
import ProfileCardFooter from "./ProfileCardFooter";
import { useContext, useEffect } from "react";
import { GithubContext } from "@/app/context/GithubContext";

interface HeaderProps {
userData: UserData;
}
export default function ProfileCard({ userData }: HeaderProps) {
const { setUser } = useContext(GithubContext);

useEffect(() => {
setUser(userData);
}, [userData, setUser]);

return (
<Card className="h-fit shadow-lg shadow-black">
<ProfileCardHeader userData={userData} />
Expand Down
4 changes: 2 additions & 2 deletions components/ProfileCard/ProfileCardFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SocialLinks from "./SocialLinks";
import { checkEmail, createUrlObject, isGithubProfile } from "@/lib/utils";
import { checkEmail, createUrlObject } from "@/lib/utils";
import { MdEmail, MdOutlineWorkOutline } from "react-icons/md";
import { TfiWorld } from "react-icons/tfi";
import { Box, Link, Text } from "@radix-ui/themes";
import { Box, Link } from "@radix-ui/themes";
import Readme from "@/components/Readme";
import ContactList from "./ContactList";
import CustomTextArea from "../ui/CustomTextArea";
Expand Down
2 changes: 0 additions & 2 deletions components/ProfileCard/ProfileCardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { HiLocationMarker } from "react-icons/hi";
import { MdEmail } from "react-icons/md";
import { FaUser } from "react-icons/fa";
import { GrOrganization } from "react-icons/gr";
import { Separator } from "@radix-ui/react-select";
import { isUrl } from "@/lib/utils";
import CustomTextArea from "../ui/CustomTextArea";
export default function ProfileCardHeader({
userData,
Expand Down
4 changes: 2 additions & 2 deletions components/Readme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export default function Readme({ url, children }: ReadmeProps) {
try {
const response = await fetch(url);
// Check if the response is not OK and throw an error
if (!response.ok) {
if (response.status !== 200) {
console.log(`Failed to fetch README.md: ${url}`);
}
const text = await response.text();
// Check if the content is not the GitHub 404 message
text === "404: Not Found" ? setError(true) : setContent(text);
} catch (err) {
// Catch any network or other errors and set the error state
console.error(err);
console.log(err);
setError(true);
}
};
Expand Down
25 changes: 20 additions & 5 deletions components/stats/Charts/StatTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { GithubContext } from "@/app/context/GithubContext";
import { StatsContext } from "@/app/context/StatsContext";
import "@/app/globals.css";
import { getProfileAge, getProfileAgeString } from "@/lib/utils";
import { Card, Grid, Heading, Text } from "@radix-ui/themes";
import { useContext } from "react";

export default function StatTable({}: {}) {
const statsContext = useContext(StatsContext);
const userContext = useContext(GithubContext);
const {
totalRepos,
totalForks,
Expand All @@ -13,9 +16,21 @@ export default function StatTable({}: {}) {
averageStarsPerRepo,
totalTopics,
} = statsContext ?? {};

const { user } = userContext;
return (
<Card className="">
<Grid
columns="2"
width="auto"
className="rounded-xl p-2 hover:bg-black/30"
>
<Heading size="4">Profile Age</Heading>
<Text>
{getProfileAgeString(
new Date(user?.created_at ? user?.created_at : ""),
)}{" "}
</Text>
</Grid>
<Grid
columns="2"
width="auto"
Expand All @@ -37,16 +52,16 @@ export default function StatTable({}: {}) {
width="auto"
className="rounded-xl p-2 hover:bg-black/30"
>
<Heading size="4">Total Forks</Heading>
<Text>{totalForks}</Text>
<Heading size="4">Total Stars</Heading>
<Text>{totalStars ?? 0}</Text>
</Grid>
<Grid
columns="2"
width="auto"
className="rounded-xl p-2 hover:bg-black/30"
>
<Heading size="4">Total Stars</Heading>
<Text>{totalStars ?? 0}</Text>
<Heading size="4">Total Forks</Heading>
<Text>{totalForks}</Text>
</Grid>
<Grid
columns="2"
Expand Down
122 changes: 122 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

/**
* Dynamically generates the site URL based on the current environment.
*
* Checks if the environment is production, then returns the appropriate
* base URL for that environment - either the NEXT_PUBLIC_URL env var in
* production, or localhost in development.
*/
export const getSiteUrl = (): string => {
// Dynamically generate the site URL based on the environment in which the page is running.

Expand All @@ -27,6 +34,13 @@ export const getSiteUrl = (): string => {
return baseUrl;
};

/**
* Creates a URL object from the provided link string.
* Validates that the link is a non-empty string before parsing.
* Prepends 'https://' if the link does not start with 'http'.
*
* @param link - The link string to parse into a URL object.
*/
export const createUrlObject = (link: string) => {
if (!link) {
throw new Error("Link is empty");
Expand All @@ -40,12 +54,28 @@ export const createUrlObject = (link: string) => {
return url;
};

/**
* Checks if the given email address matches a valid email format.
*
* @param email - The email address to validate
* @returns True if the email is valid, false otherwise
*/
export const checkEmail = (email: string) => {
const regex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regex.test(String(email).toLowerCase());
};

/**
* Fetches user contact data from the server API.
*
* @param username - The username to fetch data for
* @param option - The data option to fetch
* @param page - The page number of results to return
* @param signal - An AbortSignal to abort the fetch
* @returns A promise resolving to the user data array
* @throws Error on fetch failure
*/
export async function fetchContact(
username: string,
option: string,
Expand Down Expand Up @@ -104,6 +134,15 @@ export function formatNumber(number: number) {
}
}

/**
* Extracts unique string values from an array of objects
* by key and optional subKey.
*
* @param items - The array of objects to extract values from
* @param key - The key to extract values from each object
* @param subKey - Optional sub-key if value is an object
* @returns An array of unique string values
*/
export const extractUniqueValues = <T, K extends keyof T>(
items: T[],
key: K,
Expand Down Expand Up @@ -140,19 +179,102 @@ export default function getFormattedDate(dateString: string): string {
return formatter.format(date);
}

/**
* Checks if a string is a valid URL.
*
* @param word - The string to check.
* @returns True if the string is a valid URL, false otherwise.
*/
export const isUrl = (word: string) => {
return word.match(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/,
);
};

/**
* Checks if a given string matches a Github username pattern.
*
* @param word - The string to check
* @returns True if the string starts with '@', false otherwise
*/
export const isGithubProfile = (word: string): boolean => {
const match = word.match(/^@/);
return match !== null;
};

/**
* Converts a Unix timestamp (seconds since epoch) to a Date object.
*
* @param unixTimestamp - The Unix timestamp to convert.
* @returns The Date object for the timestamp.
*/
export function convertUnixTimestampToDate(unixTimestamp: number): Date {
const milliseconds = unixTimestamp * 1000;
const date = new Date(milliseconds);
return date;
}

/**
* Calculates the age in years based on the given Date object.
*
* Accounts for leap years by checking the month and day differences.
* Returns the age in years, months, or days depending on the differences.
*/
export function getProfileAge(dateString: Date): number {
const currentDate = new Date();
const age = currentDate.getFullYear() - dateString.getFullYear();
const monthsDiff = currentDate.getMonth() - dateString.getMonth();
const daysDiff = currentDate.getDate() - dateString.getDate();

if (
monthsDiff < 0 ||
(monthsDiff === 0 && currentDate.getDate() < dateString.getDate())
) {
return age - 1;
}

if (age === 0) {
if (monthsDiff === 0) {
return daysDiff;
}
return monthsDiff;
}

return age;
}

/**
* Returns a human-readable string representing the age difference
* between the given date and the current date.
* @param {Date} dateString The date to calculate the age from.
* @returns {string} A string representing the age difference.
*/
export function getProfileAgeString(dateString: Date): string {
const currentDate = new Date();
const age = currentDate.getFullYear() - dateString.getFullYear();
const monthsDiff = currentDate.getMonth() - dateString.getMonth();
const daysDiff = currentDate.getDate() - dateString.getDate();
const hoursDiff = currentDate.getHours() - dateString.getHours();
const minutesDiff = currentDate.getMinutes() - dateString.getMinutes();
let pluralSuffix = age - 1 === 1 ? "" : "s";

if (
monthsDiff < 0 ||
(monthsDiff === 0 && currentDate.getDate() < dateString.getDate())
) {
return `${age - 1} year${pluralSuffix}`;
}

if (age === 0) {
if (monthsDiff === 0) {
if (daysDiff === 0) {
const hourString = hoursDiff === 1 ? "" : "s";
const minuteString = minutesDiff === 1 ? "" : "s";
return `${hoursDiff} hour${hourString} and ${minutesDiff} minute${minuteString}`;
}
return `${daysDiff} day${pluralSuffix}`;
}
return `${monthsDiff} month${pluralSuffix}`;
}
return `${age} year${pluralSuffix}`;
}

1 comment on commit 8c2b661

@vercel
Copy link

@vercel vercel bot commented on 8c2b661 Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.