diff --git a/src/app/activity/page.tsx b/src/app/activity/page.tsx new file mode 100644 index 0000000..8c98346 --- /dev/null +++ b/src/app/activity/page.tsx @@ -0,0 +1,49 @@ +"use client"; +import { Activity } from "@/components/activity"; +import { Review } from "@/components/review"; +import { Rule } from "@/components/rule"; +import { Testimonials } from "@/config/Testimonials"; +import { useEffect, useState } from "react"; + +const Reviews = (): JSX.Element => { + const [activity, setActivity] = useState< + { + type: string; + date: Date; + repo: string; + repoName: string; + }[] + >([]); + + useEffect(() => { + fetch("/api/activity") + .then((data) => data.json()) + .then((data) => setActivity(data)); + }, []); + + return ( + <> + + Recent Activity + + See what Naomi has been up to lately. + + + {activity.map((act, i) => ( + + ))} + + + + > + ); +}; + +export default Reviews; diff --git a/src/app/api/activity/route.ts b/src/app/api/activity/route.ts new file mode 100644 index 0000000..ab038b4 --- /dev/null +++ b/src/app/api/activity/route.ts @@ -0,0 +1,17 @@ +import { getCodebergData } from "@/lib/codeberg"; +import { getGithubData } from "@/lib/github"; +import { NextResponse } from "next/server"; + +export async function GET() { + const codeberg = await getCodebergData(); + const github = await getGithubData(); + + const normalised: { + type: string; + date: Date; + repo: string; + repoName: string; + }[] = [...codeberg.map(i => ({ type: i.op_type, date: new Date(i.created), repo: i.repo.html_url, repoName: i.repo.full_name })), ...github.map(i => ({ type: i.type, date: new Date(i.created_at), repo: i.repo.url.replace("api.github.com/repos", "github.com"), repoName: i.repo.name }))] + + return NextResponse.json(normalised.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 100)) +} diff --git a/src/components/activity.tsx b/src/components/activity.tsx new file mode 100644 index 0000000..b8afbe4 --- /dev/null +++ b/src/components/activity.tsx @@ -0,0 +1,55 @@ +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Rule } from "./rule"; + +interface ActivityProps { + type: string; + date: Date; + repo: string; + repoName: string; + heart: string; +} + +const TypeToString: Record = { + commit_repo: "committed to", + delete_branch: "deleted a branch on", + merge_pull_request: "merged a PR in", + create_pull_request: "created a PR in", + create_branch: "created a branch in", + PushEvent: "committed to", + DeleteEvent: "deleted a branch on", + PullRequestEvent: "created or merged a PR in", + PullRequestReviewEvent: "reviewed a PR in", + PullRequestReviewCommentEvent: "commented on a PR in", + IssueCommentEvent: "commented on", + IssuesEvent: "created or updated an issue in", + close_issue: "closed an issue in", + create_issue: "created an issue in", +}; + +export const Activity = (props: ActivityProps): JSX.Element => { + const { type, date, repo, repoName, heart } = props; + return ( + + + {heart} + + + + {date.toLocaleString("en-GB")} + + + Naomi has {TypeToString[type] ?? "performed a " + type}{" "} + + {repoName} + {" "} + + + + ); +}; diff --git a/src/config/NavItems.ts b/src/config/NavItems.ts index 013cae2..07e0c6f 100644 --- a/src/config/NavItems.ts +++ b/src/config/NavItems.ts @@ -7,5 +7,6 @@ export const NavItems = [ { href: "/reviews", text: "Reviews" }, { href: "/games", text: "Games"}, { href: "/team", text: "Our Team" }, - { href: "/polycule", text: "Polycule"} + { href: "/polycule", text: "Polycule"}, + { href: "/activity", text: "Activity"} ].sort((a, b) => a.text.localeCompare(b.text)); diff --git a/src/lib/codeberg.ts b/src/lib/codeberg.ts new file mode 100644 index 0000000..a1e1f09 --- /dev/null +++ b/src/lib/codeberg.ts @@ -0,0 +1,190 @@ +interface ActivityData { + id: number + user_id: number + op_type: string + act_user_id: number + act_user: ActUser + repo_id: number + repo: Repo + comment_id: number + comment?: Comment + ref_name: string + is_private: boolean + content: string + created: string +} + +interface ActUser { + id: number + login: string + login_name: string + source_id: number + full_name: string + email: string + avatar_url: string + html_url: string + language: string + is_admin: boolean + last_login: string + created: string + restricted: boolean + active: boolean + prohibit_login: boolean + location: string + pronouns: string + website: string + description: string + visibility: string + followers_count: number + following_count: number + starred_repos_count: number + username: string +} + +interface Repo { + id: number + owner: Owner + name: string + full_name: string + description: string + empty: boolean + private: boolean + fork: boolean + template: boolean + parent: any + mirror: boolean + size: number + language: string + languages_url: string + html_url: string + url: string + link: string + ssh_url: string + clone_url: string + original_url: string + website: string + stars_count: number + forks_count: number + watchers_count: number + open_issues_count: number + open_pr_counter: number + release_counter: number + default_branch: string + archived: boolean + created_at: string + updated_at: string + archived_at: string + permissions: Permissions + has_issues: boolean + external_tracker: ExternalTracker + has_wiki: boolean + wiki_branch: string + globally_editable_wiki: boolean + has_pull_requests: boolean + has_projects: boolean + has_releases: boolean + has_packages: boolean + has_actions: boolean + ignore_whitespace_conflicts: boolean + allow_merge_commits: boolean + allow_rebase: boolean + allow_rebase_explicit: boolean + allow_squash_merge: boolean + allow_fast_forward_only_merge: boolean + allow_rebase_update: boolean + default_delete_branch_after_merge: boolean + default_merge_style: string + default_allow_maintainer_edit: boolean + avatar_url: string + internal: boolean + mirror_interval: string + object_format_name: string + mirror_updated: string + repo_transfer: any + topics: any +} + +interface Owner { + id: number + login: string + login_name: string + source_id: number + full_name: string + email: string + avatar_url: string + html_url: string + language: string + is_admin: boolean + last_login: string + created: string + restricted: boolean + active: boolean + prohibit_login: boolean + location: string + pronouns: string + website: string + description: string + visibility: string + followers_count: number + following_count: number + starred_repos_count: number + username: string +} + +interface Permissions { + admin: boolean + push: boolean + pull: boolean +} + +interface ExternalTracker { + external_tracker_url: string + external_tracker_format: string + external_tracker_style: string + external_tracker_regexp_pattern: string +} + +interface Comment { + id: number + html_url: string + pull_request_url: string + issue_url: string + user: any + original_author: string + original_author_id: number + body: string + assets: any[] + created_at: string + updated_at: string +} + + +class Codeberg { + private cache: ActivityData[]; + private lastUpdate: Date | null = null; + constructor() { + this.cache = []; + } + + private async refreshData(): Promise { + const response = await fetch("https://codeberg.org/api/v1/users/naomi-lgbt/activities/feeds?only-performed-by=true"); + const data = await response.json() as ActivityData[]; + this.cache = data; + this.lastUpdate = new Date(); + return this; + } + + public async getActivities(): Promise { + // Stale after 6 hours + if (this.lastUpdate && (new Date().getTime() - this.lastUpdate.getTime()) < 6 * 60 * 60 * 1000) { + return this.cache; + } + return this.refreshData().then(() => this.cache); + } +} + +const instantiated = new Codeberg(); + +export const getCodebergData = async (): Promise => { + return await instantiated.getActivities(); +} diff --git a/src/lib/github.ts b/src/lib/github.ts new file mode 100644 index 0000000..4c19333 --- /dev/null +++ b/src/lib/github.ts @@ -0,0 +1,728 @@ +interface ActivityData { + id: string + type: string + actor: Actor + repo: Repo + payload: Payload + public: boolean + created_at: string +} + +interface Actor { + id: number + login: string + display_login: string + gravatar_id: string + url: string + avatar_url: string +} + +interface Repo { + id: number + name: string + url: string +} + +interface Payload { + repository_id?: number + push_id?: number + size?: number + distinct_size?: number + ref?: string + head?: string + before?: string + commits?: Commit[] + ref_type?: string + pusher_type?: string + action?: string + number?: number + pull_request?: PullRequest + review?: Review + issue?: Issue2 + comment?: Comment +} + +interface Commit { + sha: string + author: Author + message: string + distinct: boolean + url: string +} + +interface Author { + email: string + name: string +} + +interface PullRequest { + url: string + id: number + node_id: string + html_url: string + diff_url: string + patch_url: string + issue_url: string + number: number + state: string + locked: boolean + title: string + user: User + body?: string + created_at: string + updated_at: string + closed_at?: string + merged_at?: string + merge_commit_sha: string + assignee: any + assignees: any[] + requested_reviewers: any[] + requested_teams: any[] + labels: Label[] + milestone: any + draft: boolean + commits_url: string + review_comments_url: string + review_comment_url: string + comments_url: string + statuses_url: string + head: Head + base: Base + _links: Links + author_association: string + auto_merge: any + active_lock_reason: any + merged?: boolean + mergeable: any + rebaseable: any + mergeable_state?: string + merged_by?: MergedBy + comments?: number + review_comments?: number + maintainer_can_modify?: boolean + commits?: number + additions?: number + deletions?: number + changed_files?: number +} + +interface User { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Label { + id: number + node_id: string + url: string + name: string + color: string + default: boolean + description: string +} + +interface Head { + label: string + ref: string + sha: string + user: User2 + repo: Repo2 +} + +interface User2 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Repo2 { + id: number + node_id: string + name: string + full_name: string + private: boolean + owner: Owner + html_url: string + description: string + fork: boolean + url: string + forks_url: string + keys_url: string + collaborators_url: string + teams_url: string + hooks_url: string + issue_events_url: string + events_url: string + assignees_url: string + branches_url: string + tags_url: string + blobs_url: string + git_tags_url: string + git_refs_url: string + trees_url: string + statuses_url: string + languages_url: string + stargazers_url: string + contributors_url: string + subscribers_url: string + subscription_url: string + commits_url: string + git_commits_url: string + comments_url: string + issue_comment_url: string + contents_url: string + compare_url: string + merges_url: string + archive_url: string + downloads_url: string + issues_url: string + pulls_url: string + milestones_url: string + notifications_url: string + labels_url: string + releases_url: string + deployments_url: string + created_at: string + updated_at: string + pushed_at: string + git_url: string + ssh_url: string + clone_url: string + svn_url: string + homepage: string + size: number + stargazers_count: number + watchers_count: number + language: string + has_issues: boolean + has_projects: boolean + has_downloads: boolean + has_wiki: boolean + has_pages: boolean + has_discussions: boolean + forks_count: number + mirror_url: any + archived: boolean + disabled: boolean + open_issues_count: number + license: License + allow_forking: boolean + is_template: boolean + web_commit_signoff_required: boolean + topics: string[] + visibility: string + forks: number + open_issues: number + watchers: number + default_branch: string +} + +interface Owner { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface License { + key: string + name: string + spdx_id: string + url: string + node_id: string +} + +interface Base { + label: string + ref: string + sha: string + user: User3 + repo: Repo3 +} + +interface User3 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Repo3 { + id: number + node_id: string + name: string + full_name: string + private: boolean + owner: Owner2 + html_url: string + description: string + fork: boolean + url: string + forks_url: string + keys_url: string + collaborators_url: string + teams_url: string + hooks_url: string + issue_events_url: string + events_url: string + assignees_url: string + branches_url: string + tags_url: string + blobs_url: string + git_tags_url: string + git_refs_url: string + trees_url: string + statuses_url: string + languages_url: string + stargazers_url: string + contributors_url: string + subscribers_url: string + subscription_url: string + commits_url: string + git_commits_url: string + comments_url: string + issue_comment_url: string + contents_url: string + compare_url: string + merges_url: string + archive_url: string + downloads_url: string + issues_url: string + pulls_url: string + milestones_url: string + notifications_url: string + labels_url: string + releases_url: string + deployments_url: string + created_at: string + updated_at: string + pushed_at: string + git_url: string + ssh_url: string + clone_url: string + svn_url: string + homepage: string + size: number + stargazers_count: number + watchers_count: number + language: string + has_issues: boolean + has_projects: boolean + has_downloads: boolean + has_wiki: boolean + has_pages: boolean + has_discussions: boolean + forks_count: number + mirror_url: any + archived: boolean + disabled: boolean + open_issues_count: number + license: License2 + allow_forking: boolean + is_template: boolean + web_commit_signoff_required: boolean + topics: string[] + visibility: string + forks: number + open_issues: number + watchers: number + default_branch: string +} + +interface Owner2 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface License2 { + key: string + name: string + spdx_id: string + url: string + node_id: string +} + +interface Links { + self: Self + html: Html + issue: Issue + comments: Comments + review_comments: ReviewComments + review_comment: ReviewComment + commits: Commits + statuses: Statuses +} + +interface Self { + href: string +} + +interface Html { + href: string +} + +interface Issue { + href: string +} + +interface Comments { + href: string +} + +interface ReviewComments { + href: string +} + +interface ReviewComment { + href: string +} + +interface Commits { + href: string +} + +interface Statuses { + href: string +} + +interface MergedBy { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Review { + id: number + node_id: string + user: User4 + body: any + commit_id: string + submitted_at: string + state: string + html_url: string + pull_request_url: string + author_association: string + _links: Links2 +} + +interface User4 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Links2 { + html: Html2 + pull_request: PullRequest2 +} + +interface Html2 { + href: string +} + +interface PullRequest2 { + href: string +} + +interface Issue2 { + url: string + repository_url: string + labels_url: string + comments_url: string + events_url: string + html_url: string + id: number + node_id: string + number: number + title: string + user: User5 + labels: Label2[] + state: string + locked: boolean + assignee: any + assignees: any[] + milestone: any + comments: number + created_at: string + updated_at: string + closed_at?: string + author_association: string + active_lock_reason: any + body: string + reactions: Reactions + timeline_url: string + performed_via_github_app: any + state_reason?: string +} + +interface User5 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Label2 { + id: number + node_id: string + url: string + name: string + color: string + default: boolean + description: string +} + +interface Reactions { + url: string + total_count: number + "+1": number + "-1": number + laugh: number + hooray: number + confused: number + heart: number + rocket: number + eyes: number +} + +interface Comment { + url: string + html_url: string + issue_url?: string + id: number + node_id: string + user: User6 + created_at: string + updated_at: string + author_association: string + body: string + reactions: Reactions2 + performed_via_github_app: any + pull_request_review_id?: number + diff_hunk?: string + path?: string + commit_id?: string + original_commit_id?: string + pull_request_url?: string + _links?: Links3 + start_line?: number + original_start_line?: number + start_side?: string + line?: number + original_line?: number + side?: string + in_reply_to_id?: number + original_position?: number + position?: number + subject_type?: string +} + +interface User6 { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +interface Reactions2 { + url: string + total_count: number + "+1": number + "-1": number + laugh: number + hooray: number + confused: number + heart: number + rocket: number + eyes: number +} + +interface Links3 { + self: Self2 + html: Html3 + pull_request: PullRequest3 +} + +interface Self2 { + href: string +} + +interface Html3 { + href: string +} + +interface PullRequest3 { + href: string +} + +interface Org { + id: number + login: string + gravatar_id: string + url: string + avatar_url: string +} + + +class Github { + private cache: ActivityData[]; + private lastUpdate: Date | null = null; + constructor() { + this.cache = []; + } + + private async refreshData(): Promise { + const response = await fetch("https://api.github.com/users/naomi-lgbt/events"); + const data = await response.json() as ActivityData[]; + this.cache = data; + this.lastUpdate = new Date(); + return this; + } + + public async getActivities(): Promise { + // Stale after 1 hour + if (this.lastUpdate && (new Date().getTime() - this.lastUpdate.getTime()) < 60 * 60 * 1000) { + return this.cache; + } + return this.refreshData().then(() => this.cache); + } +} + +const instantiated = new Github(); + +export const getGithubData = async (): Promise => { + return await instantiated.getActivities(); +}
See what Naomi has been up to lately.