diff --git a/src/app/api/auth/[...nextauth]/route.js b/src/app/api/auth/[...nextauth]/route.js deleted file mode 100644 index ba1e120..0000000 --- a/src/app/api/auth/[...nextauth]/route.js +++ /dev/null @@ -1,31 +0,0 @@ -import NextAuth from "next-auth/next" -import GitHubProvider from "next-auth/providers/github" - -export const authOptions = { - providers: [ - GitHubProvider({ - clientId: process.env.GITHUB_ID ?? "", - clientSecret: process.env.GITHUB_SECRET ?? "", - }), - ], - secret: process.env.JWT_SECRET, - callbacks: { - async jwt({ token, account, profile }) { - if (account) { - token.accessToken = account.access_token - token.id = profile.id - } - return token - }, - async session({ session, token, user }) { - session.accessToken = token.accessToken - session.user.id = token.id - - return session - } - } -} - -export const handler = NextAuth(authOptions) - -export { handler as GET, handler as POST } \ No newline at end of file diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..2e8b803 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,37 @@ +import NextAuth, { NextAuthOptions } from "next-auth" +import { JWT } from "next-auth/jwt" +import GitHubProvider from "next-auth/providers/github" +import { Account, Profile, Session, User } from "next-auth" + +export const authOptions: NextAuthOptions = { + providers: [ + GitHubProvider({ + clientId: process.env.GITHUB_ID ?? "", + clientSecret: process.env.GITHUB_SECRET ?? "", + }), + ], + callbacks: { + async jwt({ token, account, profile }: { token: JWT, account: Account | null, profile?: Profile}) + { + if (account && account.access_token) { + token.accessToken = account.access_token; + } + + if (profile && (profile as any).id) { + token.id = (profile as any).id as string; + } + + return token; + }, + async session({ session, token }: { session: Session, token: JWT }) { + session.accessToken = token.accessToken; + session.user.id = token.id as string; + + return session; + } + } +} + +export const handler = NextAuth(authOptions) + +export { handler as GET, handler as POST } diff --git a/src/app/api/repos/[owner]/[repo]/commits/route.ts b/src/app/api/repos/[owner]/[repo]/commits/route.ts index c1e3888..84881b9 100644 --- a/src/app/api/repos/[owner]/[repo]/commits/route.ts +++ b/src/app/api/repos/[owner]/[repo]/commits/route.ts @@ -7,6 +7,8 @@ import fetchURL from "../../../../utils/utils"; const querySchema = z.object({ owner: z.string(), repo: z.string(), + per_page: z.string().optional(), + page: z.string().optional(), }); /** @@ -30,6 +32,18 @@ const querySchema = z.object({ * description: The name of the repository * schema: * type: string + * - name: per_page + * in: query + * required: false + * description: The number of commits per page (max 100) + * schema: + * type: integer + * - name: page + * in: query + * required: false + * description: The page number of the results to fetch + * schema: + * type: integer * produces: * - application/json * responses: @@ -75,7 +89,7 @@ const querySchema = z.object({ * description: Failed to fetch commits */ export async function GET( - req: NextRequest, {params}: any + req: NextRequest, { params }: any ) { try { const session = await getServerSession({ req, ...authOptions }); @@ -90,23 +104,46 @@ export async function GET( if (!result.success) { return NextResponse.json( - { message: "Invalid query parameters" }, - { status: 400 } - ) + { message: "Invalid query parameters" }, + { status: 400 } + ); } const { owner, repo } = result.data; + const per_page: string = result.data.per_page || "100"; + let page: string = result.data.page || "1"; + + const encodedOwner: string = encodeURIComponent(owner); + const encodedRepo: string = encodeURIComponent(repo); + + let allCommits: Array = []; + let hasMorePages: boolean = true; - const encodedOwner : string = encodeURIComponent(owner); - const encodedRepo : string = encodeURIComponent(repo); + while (hasMorePages) { + const apiUrl: string = `https://api.github.com/repos/${encodedOwner}/${encodedRepo}/commits?per_page=${per_page}&page=${page}`; + + const response = await fetchURL(req, apiUrl, "GET"); - const apiUrl : string = `https://api.github.com/repos/${encodedOwner}/${encodedRepo}/commits`; + if (!response.ok) { + return NextResponse.json( + { message: `Failed to fetch commits: ${response.statusText}` }, + { status: response.status } + ); + } - const response = await fetchURL(req, apiUrl, "GET"); + const commits: Array = await response.json(); + + allCommits = allCommits.concat(commits); + + if (commits.length < parseInt(per_page)) { + hasMorePages = false; + } else { + page = (parseInt(page) + 1).toString(); + } + } - const commits : Array = await response.json(); return NextResponse.json( - commits[0], + allCommits, { status: 200 } ); } catch (error) { diff --git a/src/app/api/repos/[owner]/[repo]/pulls/route.ts b/src/app/api/repos/[owner]/[repo]/pulls/route.ts new file mode 100644 index 0000000..c72c777 --- /dev/null +++ b/src/app/api/repos/[owner]/[repo]/pulls/route.ts @@ -0,0 +1,51 @@ +import { getServerSession } from 'next-auth'; +import { z } from "zod"; +import fetchURL from "../../../../utils/utils"; +import { authOptions } from "../../../../../api/auth/[...nextauth]/route"; +import { NextRequest, NextResponse } from "next/server"; + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), +}); + +export async function GET( + req: NextRequest, { params }: any +) { + try { + const session = await getServerSession({ req, ...authOptions }); + if (!session) { + console.error('No valid access token found in session'); + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const result = querySchema.safeParse(params); + + if (!result.success) { + return NextResponse.json( + { message: "Invalid query parameters" }, + { status: 400 } + ); + } + + const { owner, repo } = result.data + + const encodedOwner : string = encodeURIComponent(owner); + const encodedRepo : string = encodeURIComponent(repo); + + const apiUrl : string = `https://api.github.com/repos/${encodedOwner}/${encodedRepo}/pulls`; + + const response = await fetchURL(req, apiUrl, "GET"); + + const pulls : object = await response.json(); + return NextResponse.json( + pulls, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: 'Failed to fetch pulls' }, + { status: 500 } + ); + } +}; diff --git a/src/app/api/repos/[owner]/[repo]/route.ts b/src/app/api/repos/[owner]/[repo]/route.ts new file mode 100644 index 0000000..77e04fe --- /dev/null +++ b/src/app/api/repos/[owner]/[repo]/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '../../../../api/auth/[...nextauth]/route'; +import fetchURL from '../../../utils/utils'; +import { z } from 'zod'; + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), + }); + + +export async function GET( + req: NextRequest, {params}: any + ) { + try { + const session = await getServerSession({ req, ...authOptions }); + if (!session) { + return NextResponse.json( + { message: 'Unauthorized' }, + { status: 401 } + ); + } + + const result = querySchema.safeParse(params); + + if (!result.success) { + return NextResponse.json( + { message: "Invalid query parameters" }, + { status: 400 } + ) + } + + const { owner, repo } = result.data; + + const encodedOwner: string = encodeURIComponent(owner); + const encodedRepo: string = encodeURIComponent(repo); + + const apiUrl: string = `https://api.github.com/repos/${encodedOwner}/${encodedRepo}`; + + const response = await fetchURL(req, apiUrl, "GET"); + + const repository: object = await response.json(); + return NextResponse.json( + repository, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: 'Failed to fetch repository' }, + { status: 500 } + ); + } + }; + \ No newline at end of file diff --git a/src/app/api/utils/utils.ts b/src/app/api/utils/utils.ts index afabb11..4e75d8b 100644 --- a/src/app/api/utils/utils.ts +++ b/src/app/api/utils/utils.ts @@ -11,7 +11,7 @@ const fetchURL = async (req: NextRequest, url: string, method: string) => { method: method, headers: { Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', + 'Content-Type': 'application/vnd.github+json', }, }); diff --git a/src/app/globals.css b/src/app/globals.css index cfc01cf..e82d12d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -33,10 +33,27 @@ --ring: 222.2 84% 4.9%; --radius: 0.5rem; + + ::-webkit-scrollbar { + width: 10px; + } + + ::-webkit-scrollbar-track { + background: #f1f1f1; + } + + ::-webkit-scrollbar-thumb { + background: #c5c5c58f; + border-radius: 5px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #555; + } } .dark { - --background: 222.2 100% 0%; + --background: #001229; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; @@ -63,6 +80,23 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; + + ::-webkit-scrollbar { + width: 10px; + } + + ::-webkit-scrollbar-track { + background: #000000; + } + + ::-webkit-scrollbar-thumb { + background: #ffffff; + border-radius: 5px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #555; + } } } @@ -73,4 +107,18 @@ body { @apply bg-background text-foreground; } + } + + @layer components { + .max-h-transition { + transition: max-height 1s ease-out; + } + } + + .truncate-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 200px; } \ No newline at end of file diff --git a/src/app/repository/[owner]/[repos]/page.tsx b/src/app/repository/[owner]/[repos]/page.tsx new file mode 100644 index 0000000..a922281 --- /dev/null +++ b/src/app/repository/[owner]/[repos]/page.tsx @@ -0,0 +1,417 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { CircleAlert, Logs, FolderKanban, ShieldCheck, ShieldX, FileWarning, Star, LineChart, ShieldAlert, Component, TriangleAlert, Eye, GitFork } from "lucide-react"; +import VulnerabilityCard from "../../../../../src/components/ui/card/vulnerability"; +import GithubCommits from "../../../../../src/components/charts/github-commits-repos"; +import GithubContributor from "../../../../../src/components/charts/github-contributors"; +import Loading from "../../../../../src/components/ui/loading"; + + +interface ReposProps { + params: { + repos: string; + owner: string; + }; +} + +interface Repository { + id: string; + name: string; + owner?: { + avatar_url: string; + login: string; + }; + version: string; + LastPush : string; + created_at: string; + stargazers_count: number; + forks_count: number; + watchers_count: number; +} + +interface Contributor { + login: string; + avatar_url: string; + contributions: number; +} + +interface CommitResponse { + sha: string; + node_id: string; + commit: CommitDetails; + url: string; + html_url: string; + comments_url: string; + author: User; + committer: User; + parents: Parent[]; +} + +interface CommitDetails { + author: AuthorCommitter; + committer: AuthorCommitter; + message: string; + tree: Tree; + url: string; + comment_count: number; + verification: Verification; +} + +interface AuthorCommitter { + name: string; + email: string; + date: string; +} + +interface Tree { + sha: string; + url: string; +} + +interface Verification { + verified: boolean; + reason: string; + signature: string | null; + payload: string | null; +} + +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 Parent { + sha: string; + url: string; + html_url: string; +} + + +export default function RepoPage( + { params }: ReposProps +) { + + const [repositories, setRepositories] = useState(); + const [contributors, setContributors] = useState(); + const [commits, setCommits] = useState(); + const [lastCommits, setLastCommits] = useState(); + + useEffect(() => { + const fetchRepositories = async () => { + try { + const response = await fetch(`/api/repos/${params.owner}/${params.repos}`); + + if (!response.ok) { + throw new Error('Failed to fetch repositories'); + } + + const data : Repository = await response.json(); + setRepositories(data); + } catch (err) { + console.log(err); + } + } + + const fetchContributors = async () => { + try { + const response = await fetch(`/api/repos/${params.owner}/${params.repos}/contributors`); + + if (!response.ok) { + throw new Error('Failed to fetch repositories'); + } + + const data : Contributor[] = await response.json(); + setContributors(data); + } catch (err) { + console.log(err); + } + } + + const fetchCommits = async () => { + try { + const response = await fetch(`/api/repos/${params.owner}/${params.repos}/commits`); + + if (!response.ok) { + throw new Error('Failed to fetch repositories'); + } + + const data : CommitResponse[] = await response.json(); + const topFiveCommits : CommitResponse[] = data.slice(0, 5).map(commit => commit); + setLastCommits(topFiveCommits); + setCommits(data.map(commit => commit)); + } catch (err) { + console.log(err); + } + } + + fetchRepositories().catch(console.error); + fetchContributors().catch(console.error); + fetchCommits().catch(console.error); + + } + , []); + + useEffect(() => { + }, [repositories, contributors, commits]); + + const formatDate = (isoDateString: string) => { + const date: Date = new Date(isoDateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + }; + + return ( +
+
+

+ Dashboard +

+
+
+
+ +
+

+ {repositories?.name} +

+

+ {repositories?.created_at ? formatDate(repositories.created_at) : 'Loading ...'} +

+
+
+
+
+
+ +

+ {repositories?.stargazers_count} +

+

+ stars +

+
+
+ +

+ {repositories?.forks_count} +

+

+ forks +

+
+
+ +

+ {repositories?.watchers_count} +

+

+ watchers +

+
+
+
+
+
+
+ +

Commits History

+
+
+
+
+ { commits ? ( + + ) : ( +
+
+

Loading ...

+
+
+ +
+
+ )} +
+
+
+
+
+
+ +

Contributors

+
+
+ { contributors ? ( + + ) : ( +
+
+

Loading ...

+
+
+ +
+
+ )} +
+
+
+
+ +

Vulnerability History

+
+
+
+
+
+ +
+

Potential Sql Injection

+

Yesterday

+
+
+
+
+

High

+
+
+
+
+
+
+ +
+

Token Leak

+

Jun 23, 2024, 11:30:00PM

+
+
+
+
+

Very High

+
+
+
+
+
+
+ +
+

Cross-Site Scripting

+

Jun 16, 2024, 11:30:00PM

+
+
+
+
+

Low

+
+
+
+
+
+
+ +
+

Cross-Site Scripting

+

Jun 16, 2024, 11:30:00PM

+
+
+
+
+

Low

+
+
+
+
+
+
+
+
+
+
+

+ Overview +

+
+
+ +

Vulnerability

+
+ + +
+
+ +

Logs -> Last Commits

+
+
+ {lastCommits ? ( + lastCommits.map((commit, index) => ( + +
+
+
+ User avatar +
+

{commit.author.login}

+

{commit.commit.message}

+

{formatDate(commit.commit.author.date)}

+
+
+
+
+ {index % 2 === 0 && ( + + )} +
+
+ {index < lastCommits.length - 1 && ( +
+ )} +
+ )) + ) : ( +
+
+

Loading ...

+
+
+ +
+
+ )} +
+
+
+
+
+
+ ); + +} \ No newline at end of file diff --git a/src/app/user/me/page.tsx b/src/app/user/me/page.tsx index 53ea8e8..9ed1cc1 100644 --- a/src/app/user/me/page.tsx +++ b/src/app/user/me/page.tsx @@ -12,10 +12,42 @@ interface UserProps { }; } +interface UserData { + login: string; + avatar_url: string; + name: string; + bio: string; + location: string; + followers: number; + following: number; + public_repos: number; + total_private_repos: number; + plan: { + name: string; + collaborators: number; + private_repos: number; + storage: { + used: number; + total: number; + }; + }; +} + +interface UserContributions { + total: { + total: number; + year: number; + }; + contributions: { + date: string; + count: number; + }[]; +} + export default function UserPage({ params }: UserProps) { const { username } = params; - const [userData, setUserData] = useState(null); - const [userContributions, setUserContributions] = useState(null); + const [userData, setUserData] = useState(null); + const [userContributions, setUserContributions] = useState(null); const [loading, setLoading] = useState(true); const [contributionsLoading, setContributionsLoading] = useState(true); @@ -92,7 +124,7 @@ export default function UserPage({ params }: UserProps) {
-
+

FOLLOWERS

{userData.followers}

@@ -101,7 +133,7 @@ export default function UserPage({ params }: UserProps) {
-
+

FOLLOWING

{userData.following}

@@ -110,7 +142,7 @@ export default function UserPage({ params }: UserProps) {
-
+

PUBLIC REPOS

{userData.public_repos}

@@ -119,7 +151,7 @@ export default function UserPage({ params }: UserProps) {
-
+

PRIVATE REPOS

{userData.total_private_repos}

@@ -132,19 +164,19 @@ export default function UserPage({ params }: UserProps) {
-
+

TOTAL CONTRIBUTION

- {contributionsLoading ? ( + {contributionsLoading || !userContributions ? (
Loading contributions...
- ) : ( + ) : ( - )} + )}
-
+

PLAN

@@ -208,11 +240,11 @@ export default function UserPage({ params }: UserProps) {

Github Contributions

- {contributionsLoading ? ( + {contributionsLoading || !userContributions ? (
Loading contributions...
- ) : ( + ) : ( - )} + )}
diff --git a/src/components/auth/navbar.tsx b/src/components/auth/navbar.tsx index dad278d..218f11d 100644 --- a/src/components/auth/navbar.tsx +++ b/src/components/auth/navbar.tsx @@ -3,14 +3,12 @@ import { Menu, X, Moon, Sun } from "lucide-react"; import { useState, useEffect } from "react"; import logo from "../../../assets/secure-CI.png"; -import { navItems } from "../../constants"; import Image from "next/image"; import { signIn } from "next-auth/react"; -import { boolean } from "zod"; const Navbar = () => { const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); - const [darkMode, setDarkMode] = useState(false); + const [darkMode, setDarkMode] = useState(false); const toggleNavbar = () => { setMobileDrawerOpen(!mobileDrawerOpen); @@ -26,14 +24,17 @@ const Navbar = () => { }, [darkMode]); useEffect(() => { - const mode = localStorage.getItem("darkMode"); - if (mode === "true") { + const mode: string | null = localStorage.getItem("darkMode"); + const isDarkMode = mode === "true"; + + if (isDarkMode) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } - setDarkMode(mode); - } , []); + + setDarkMode(isDarkMode); + }, []); const toggleDarkMode = () => { setDarkMode(!darkMode); @@ -70,7 +71,7 @@ const Navbar = () => { Get started
diff --git a/src/components/auth/testimonials.tsx b/src/components/auth/testimonials.tsx index b668684..1397307 100644 --- a/src/components/auth/testimonials.tsx +++ b/src/components/auth/testimonials.tsx @@ -10,7 +10,7 @@ const Testimonials = () => {
{testimonials.map((testimonial, index) => (
-
+

{testimonial.text}

diff --git a/src/components/charts/github-commits-repos.jsx b/src/components/charts/github-commits-repos.jsx new file mode 100644 index 0000000..a3f59f5 --- /dev/null +++ b/src/components/charts/github-commits-repos.jsx @@ -0,0 +1,142 @@ +import { useEffect } from "react"; +import * as am5 from "@amcharts/amcharts5"; +import * as am5xy from "@amcharts/amcharts5/xy"; +import * as am5themes_Animated from "@amcharts/amcharts5/themes/Animated"; +import * as am5themes_Dark from "@amcharts/amcharts5/themes/Dark"; + +const ChartComponent = (props) => { + useEffect(() => { + if (!props.commits || props.commits.length === 0) { + return; + } + + let root = am5.Root.new("chartdiv4"); + + const isDarkMode = document.documentElement.classList.contains("dark"); + + root.setThemes([am5themes_Animated.default.new(root)]); + if (isDarkMode) { + root.setThemes([am5themes_Dark.default.new(root)]); + } + + let chart = root.container.children.push(am5xy.XYChart.new(root, { + panX: true, + panY: true, + wheelX: "panX", + wheelY: "zoomX", + pinchZoomX: true, + paddingLeft: 0 + })); + + let cursor = chart.set("cursor", am5xy.XYCursor.new(root, { + behavior: "none", + lineX: am5.Line.new(root, { visible: true, strokeDasharray: [3, 3] }), + lineY: am5.Line.new(root, { visible: true, strokeDasharray: [3, 3] }) + })); + + cursor.lineY.set("visible", true); + cursor.lineX.set("visible", true); + + const commitsByDate = {}; + props.commits.forEach(commit => { + const date = new Date(commit.commit.author.date); + const dateString = date.toISOString().split('T')[0]; + if (!commitsByDate[dateString]) { + commitsByDate[dateString] = 0; + } + commitsByDate[dateString]++; + }); + + const data = Object.keys(commitsByDate).map(date => ({ + date: new Date(date).getTime(), + value: commitsByDate[date] + })); + + let xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, { + baseInterval: { timeUnit: "day", count: 1 }, + renderer: am5xy.AxisRendererX.new(root, { + minorGridEnabled: true + }), + tooltip: am5.Tooltip.new(root, {}) + })); + + let yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, { + renderer: am5xy.AxisRendererY.new(root, { + minGridDistance: 40 + }) + })); + + let series = chart.series.push(am5xy.LineSeries.new(root, { + name: "Series", + xAxis: xAxis, + yAxis: yAxis, + valueYField: "value", + valueXField: "date", + tooltip: am5.Tooltip.new(root, { + labelText: "{valueY} commits" + }) + })); + + let scrollbar = chart.set("scrollbarX", am5xy.XYChartScrollbar.new(root, { + orientation: "horizontal", + height: 60 + })); + + let sbDateAxis = scrollbar.chart.xAxes.push(am5xy.DateAxis.new(root, { + baseInterval: { timeUnit: "day", count: 1 }, + renderer: am5xy.AxisRendererX.new(root, { + minorGridEnabled: true, + minGridDistance: 70 + }) + })); + + let sbValueAxis = scrollbar.chart.yAxes.push(am5xy.ValueAxis.new(root, { + renderer: am5xy.AxisRendererY.new(root, { + minGridDistance: 40 + }) + })); + + let sbSeries = scrollbar.chart.series.push(am5xy.LineSeries.new(root, { + valueYField: "value", + valueXField: "date", + xAxis: sbDateAxis, + yAxis: sbValueAxis + })); + + series.fills.template.setAll({ + fillOpacity: 0.2, + visible: true + }); + + series.data.setAll(data); + sbSeries.data.setAll(data); + + function createAxisRange(axis, startDate, endDate, chart) { + let dataItem = axis.makeDataItem({}); + dataItem.set("value", startDate.getTime()); + dataItem.set("endValue", endDate.getTime()); + + let range = axis.createAxisRange(dataItem); + range.get("axisFill").setAll({ + fill: chart.get("colors").getIndex(7), + fillOpacity: 0.4, + visible: true + }); + range.get("grid").set("forceHidden", true); + } + + createAxisRange(xAxis, new Date(2023, 4, 10), new Date(2023, 4, 11), chart); + createAxisRange(xAxis, new Date(2023, 4, 19), new Date(2023, 4, 20), chart); + + series.appear(1000); + chart.appear(1000, 100); + + return () => { + root.dispose(); + }; + }, [props.commits]); + + return
; +} + +export default ChartComponent; diff --git a/src/components/charts/github-contributors.jsx b/src/components/charts/github-contributors.jsx new file mode 100644 index 0000000..711a4c4 --- /dev/null +++ b/src/components/charts/github-contributors.jsx @@ -0,0 +1,129 @@ +import { useEffect } from "react"; +import * as am5 from "@amcharts/amcharts5"; +import * as am5xy from "@amcharts/amcharts5/xy"; +import * as am5themes_Animated from "@amcharts/amcharts5/themes/Animated"; +import * as am5themes_Dark from "@amcharts/amcharts5/themes/Dark"; + +const ChartComponent = (props) => { + + useEffect(() => { + + let root = am5.Root.new("chartdiv5"); + + const isDarkMode = document.documentElement.classList.contains("dark"); + + root.setThemes([am5themes_Animated.default.new(root)]); + + if (isDarkMode) { + root.setThemes([am5themes_Dark.default.new(root)]); + } + + let chart = root.container.children.push(am5xy.XYChart.new(root, { + panX: false, + panY: false, + wheelX: "none", + wheelY: "none", + paddingLeft: 0 + })); + + let cursor = chart.set("cursor", am5xy.XYCursor.new(root, {})); + cursor.lineY.set("visible", false); + + let xRenderer = am5xy.AxisRendererX.new(root, { + minGridDistance: 30, + minorGridEnabled: true + }); + + let xAxis = chart.xAxes.push(am5xy.CategoryAxis.new(root, { + maxDeviation: 0, + categoryField: "name", + renderer: xRenderer, + tooltip: am5.Tooltip.new(root, {}) + })); + + xRenderer.grid.template.set("visible", false); + + xRenderer.labels.template.set("visible", false); + + let yRenderer = am5xy.AxisRendererY.new(root, {}); + let yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, { + maxDeviation: 0, + min: 0, + extraMax: 0.1, + renderer: yRenderer + })); + + yRenderer.grid.template.setAll({ + strokeDasharray: [2, 2] + }); + + let series = chart.series.push(am5xy.ColumnSeries.new(root, { + name: "Series 1", + xAxis: xAxis, + yAxis: yAxis, + valueYField: "value", + sequencedInterpolation: true, + categoryXField: "name", + tooltip: am5.Tooltip.new(root, { dy: -25, labelText: "{valueY}" }) + })); + + series.columns.template.setAll({ + cornerRadiusTL: 5, + cornerRadiusTR: 5, + strokeOpacity: 0 + }); + + series.columns.template.adapters.add("fill", (fill, target) => { + return chart.get("colors").getIndex(series.columns.indexOf(target)); + }); + + series.columns.template.adapters.add("stroke", (stroke, target) => { + return chart.get("colors").getIndex(series.columns.indexOf(target)); + }); + + console.log(props.contributors); + + let data = []; + + if (props.contributors && props.contributors.length > 0) { + data = props.contributors.map(contributor => ({ + name: contributor.login, + value: contributor.contributions, + bulletSettings: { src: contributor.avatar_url } + })); + } + + series.bullets.push(function() { + return am5.Bullet.new(root, { + locationY: 1, + sprite: am5.Picture.new(root, { + templateField: "bulletSettings", + width: 50, + height: 50, + centerX: am5.p50, + centerY: am5.p50, + shadowColor: am5.color(0x000000), + shadowBlur: 4, + shadowOffsetX: 4, + shadowOffsetY: 4, + shadowOpacity: 0.6 + }) + }); + }); + + xAxis.data.setAll(data); + series.data.setAll(data); + + series.appear(1000); + chart.appear(1000, 100); + + return () => { + root.dispose(); + } + + }, [props.contributors]); + + return
; +}; + +export default ChartComponent; diff --git a/src/components/ui/card/vulnerability.tsx b/src/components/ui/card/vulnerability.tsx new file mode 100644 index 0000000..3ae5171 --- /dev/null +++ b/src/components/ui/card/vulnerability.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React, { useState } from "react"; +import { TriangleAlert, ChevronDown, Dot } from "lucide-react"; + +const Vulnerability = () => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const toggleDropdown = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + return ( +
+
+
+ +

Potential SQL Injection

+ +
+
+
+

Yesterday

+
+
+
+

Severity:

+

High

+
+
+
+
+

Location:

+
+ +

File: src/db/query.js

+
+
+ +

Line: 27

+
+
+
+
+
+
+
+ ); +} + +export default Vulnerability; diff --git a/src/components/ui/loading.tsx b/src/components/ui/loading.tsx new file mode 100644 index 0000000..e75a9e2 --- /dev/null +++ b/src/components/ui/loading.tsx @@ -0,0 +1,16 @@ +import { useEffect } from "react"; + +const Loading = () => { + + return ( +
+ + Loading... +
+ ) +}; + +export default Loading; \ No newline at end of file diff --git a/src/components/ui/side-bar.tsx b/src/components/ui/side-bar.tsx index 8ff747b..111fbe8 100644 --- a/src/components/ui/side-bar.tsx +++ b/src/components/ui/side-bar.tsx @@ -1,7 +1,9 @@ "use client"; -import {LogOut, Plus, User, Moon, Sun, Folder} from "lucide-react"; +import { LogOut, Plus, User, FolderKanban, ChevronRight, Moon, Sun, House, LayoutDashboard, Building } from "lucide-react"; import { useEffect, useState } from "react"; +import logo from "../../../assets/secure-CI.png"; +import Image from "next/image"; import { DropdownMenu, DropdownMenuContent, @@ -22,23 +24,54 @@ interface SideBarChildrenProps { currentOrg: Organization; } +interface Repository { + name: string; + owner: { + login: string; + }; +} + const SideBar = ({ organizations, currentOrg }: SideBarChildrenProps) => { + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const [Repos, setRepos] = useState([]); + + const toggleDropdown = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + const { data: session } = useSession(); const router = useRouter(); - const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [darkMode, setDarkMode] = useState(false); - const toggleNavbar = () => { - setMobileDrawerOpen(!mobileDrawerOpen); - }; - useEffect(() => { const isDarkMode = document.documentElement.classList.contains("dark"); setDarkMode(isDarkMode); }, []); + useEffect(() => { + const fetchRepos = async () => { + try { + const response = await fetch("/api/repos/repository") + + if (!response.ok) { + throw new Error('Failed to fetch repositories'); + } + + const data: Repository[] = await response.json(); + setRepos(data); + + } catch (err) { + console.error(err); + } + } + + fetchRepos().catch(console.error); + }, []); + const toggleDarkMode = () => { setDarkMode((prevDarkMode) => { const newDarkMode = !prevDarkMode; @@ -62,75 +95,95 @@ const SideBar = ({ organizations, currentOrg }: SideBarChildrenProps) => { className="fixed left-0 top-0 z-40 h-screen w-64 transition-transform" aria-label="Sidebar" > -
-
- - - - - - - - - - - - - {currentOrg.name} - - - {organizations?.map((org) => ( - - {org.name} +
+
+
+ Secure-CI +

Secure-CI

+
+
+
+ +
+ + + {currentOrg.name} + + + {organizations?.map((org) => ( + + {org.name} + + ))} + + + Add org + + + + + router.replace('/user/me')}> + {session?.user?.name} + - ))} - - - Add org - - - - - router.replace('/user/me')}> - {session?.user?.name} - - - - - router.replace('/repository')}> - Repositories - - - - - {darkMode ? : } - - {darkMode ? "Dark mode" : "Light mode"} - - - - - signOut({callbackUrl: '/'})}>Log out - - - + + {darkMode ? : } + + {darkMode ? "Dark mode" : "Light mode"} + + + + + signOut({callbackUrl: '/'})}>Log out + + + +
+
+
+
router.replace('/dashboard')} className="ml-2 mt-5 p-2 flex w-11/12 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700"> + + +
+
+
+ + +
+
+ +
+
+
+
+ {Repos.map((repo) => ( +
+ + +
+ ))} +
+
+
+
router.replace('/user/me')} className="ml-2 p-2 flex w-11/12 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700"> + User avatar +
- ); + ); }; export { SideBar }; diff --git a/src/components/ui/theme-provider.tsx b/src/components/ui/theme-provider.tsx index 7ee3d9c..455461f 100644 --- a/src/components/ui/theme-provider.tsx +++ b/src/components/ui/theme-provider.tsx @@ -3,30 +3,16 @@ import { Moon, Sun } from "lucide-react"; import { useEffect, useState } from "react"; export default function ThemeProvider({ children }: { children: React.ReactNode }) { - const [isDarkMode, setIsDarkMode] = useState(false); useEffect(() => { const mode = localStorage.getItem("darkMode"); if (mode === "true") { document.documentElement.classList.add("dark"); - setIsDarkMode(true); } else { document.documentElement.classList.remove("dark"); - setIsDarkMode(false); } }, []); - const toggleDarkMode = () => { - const newDarkMode = !isDarkMode; - localStorage.setItem("darkMode", newDarkMode ? "true" : "false"); - if (newDarkMode) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } - setIsDarkMode(newDarkMode); - }; - return ( <> {children} diff --git a/tailwind.config.ts b/tailwind.config.ts index cec4214..246032d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -22,13 +22,15 @@ const config = { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", - background: "hsl(var(--background))", + background: "var(--background)", foreground: "hsl(var(--foreground))", 'dark-background': 'rgba(25, 25, 25, 0.83)', 'plan-background': 'rgba(255, 3, 41, 0.37)', 'collab-background': 'rgba(3, 137, 255, 0.37)', 'private-background': 'rgba(255, 3, 179, 0.28)', 'followers-background': 'rgba(3, 255, 58, 0.24)', + 'semi-light': 'rgba(233, 242, 253, 0.8)', + 'semi-dark': 'rgba(37, 37, 37, 0.77)', primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", @@ -77,6 +79,11 @@ const config = { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, + height: { + '110': '28rem', + '128': '32rem', + '144': '36rem', + }, }, }, plugins: [require("tailwindcss-animate")], diff --git a/types/next-auth-d.ts b/types/next-auth-d.ts index fa42fa5..36b8c04 100644 --- a/types/next-auth-d.ts +++ b/types/next-auth-d.ts @@ -7,6 +7,7 @@ declare module "next-auth" { accessToken: string; user: { id: string; + name: string; } }