Skip to content

Commit

Permalink
Merge pull request #108 from kudos-ink/beta
Browse files Browse the repository at this point in the history
Beta
  • Loading branch information
ipapandinas authored Oct 9, 2024
2 parents 7665076 + 98a2cba commit cb356f5
Show file tree
Hide file tree
Showing 79 changed files with 8,012 additions and 5,184 deletions.
48 changes: 48 additions & 0 deletions api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
type Options = {
tag?: string;
noStoreCache?: boolean;
};

export default class APIClient {
private baseURL: string;

constructor(baseURL: string) {
this.baseURL = baseURL;
}

private async request<T>(url: string, options: RequestInit): Promise<T> {
const response = await fetch(`${this.baseURL}${url}`, options);
if (!response.ok) {
const error = new Error("HTTP Error") as any;
error.status = response.status;
error.response = await response.json();
throw error;
}
return (await response.json()) as T;
}

public get<T>(url: string, options: Options = {}): Promise<T> {
const fetchOptions: RequestInit = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
...(options.tag ? { next: { tags: [options.tag] } } : {}),
...(options.noStoreCache ? { cache: "no-store" } : {}),
};
return this.request<T>(url, fetchOptions);
}

public post<T, D>(url: string, data: D, options: Options = {}): Promise<T> {
const fetchOptions: RequestInit = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
...(options.tag ? { next: { tags: [options.tag] } } : {}),
...(options.noStoreCache ? { cache: "no-store" } : {}),
};
return this.request<T>(url, fetchOptions);
}
}
7 changes: 7 additions & 0 deletions api/config/_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { sanitizeUrl } from "@/utils/url";
import APIClient from "@/api/client";

const CONFIG_API_URL = sanitizeUrl(
process.env.PROJECT_CLASSIFICATION_URL || "",
);
export const configApiClient = new APIClient(CONFIG_API_URL);
13 changes: 13 additions & 0 deletions api/config/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ProjectInfos } from "@/types/project";
import tags from "@/utils/tags";
import { configApiClient } from "./_client";

const PROJECTS_PATH = "/projects";

export async function getProjectInfos(slug: string): Promise<ProjectInfos> {
const url = `${PROJECTS_PATH}/${slug}.json`;
const tag = tags.projectInfos(slug);
return configApiClient.get<ProjectInfos>(url, { tag });
}

export default { getProjectInfos };
5 changes: 5 additions & 0 deletions api/core/_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { sanitizeUrl } from "@/utils/url";
import APIClient from "@/api/client";

const CORE_API_URL = sanitizeUrl(process.env.NEXT_PUBLIC_API_URL || "");
export const coreApiClient = new APIClient(CORE_API_URL);
79 changes: 79 additions & 0 deletions api/core/_transformers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { GOOD_FIRST_ISSUE_LABELS, KUDOS_ISSUE_LABELS } from "@/data/filters";
import {
Issue,
IssueDto,
IssueQueryParamsWithPagination,
IssueQueryParamsDto,
} from "@/types/issue";
import { Project, ProjectDto } from "@/types/project";
import { Repository, RepositoryDto } from "@/types/repository";

export function dtoToIssue(dto: IssueDto): Issue {
return {
id: dto.id,
issueId: dto.issue_id,
isCertified: dto.certified,
labels: dto.labels ?? [],
repository: dtoToRepository(dto.repository),
project: dtoToProject(dto.repository.project),
title: dto.title,
createdAt: dto.issue_created_at,
url: dto.repository.url + `/issues/${dto.issue_id}`,
};
}

export function dtoToRepository(dto: RepositoryDto): Repository {
return {
id: dto.id,
slug: dto.slug,
language: dto.language_slug,
// project: dtoToProject(dto.project),
name: dto.name,
url: dto.url,
};
}

export function dtoToProject(dto: ProjectDto): Project {
return {
id: dto.id,
name: dto.name,
slug: dto.slug,
avatar: dto.avatar ?? null,
categories: dto.categories ?? [],
purposes: dto.purposes ?? [],
stack_levels: dto.stack_levels ?? [],
technologies: dto.technologies ?? [],
};
}

export function issueQueryParamsToDto(
query: IssueQueryParamsWithPagination,
allLanguages: string[],
): IssueQueryParamsDto {
const { technologies = [], labels = [], goodFirst } = query;

const languageSlugs = technologies.filter((tech) =>
allLanguages.includes(tech),
);
const remainingTechnologies = technologies.filter(
(tech) => !allLanguages.includes(tech),
);

const combinedLabels = goodFirst
? [...labels, ...GOOD_FIRST_ISSUE_LABELS, ...KUDOS_ISSUE_LABELS]
: labels;

return {
slugs: query.projects,
certified: query.certified,
purposes: query.purposes,
stack_levels: query.stackLevels,
labels: combinedLabels.length > 0 ? combinedLabels : undefined,
open: query.open,
technologies:
remainingTechnologies.length > 0 ? remainingTechnologies : undefined,
language_slugs: languageSlugs.length > 0 ? languageSlugs : undefined,
offset: query.offset,
limit: query.limit,
};
}
36 changes: 36 additions & 0 deletions api/core/issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DEFAULT_QUERY } from "@/data/fetch";
import { IssueQueryParams, IssueDto, Issue } from "@/types/issue";
import {
PaginationQueryParams,
PaginatedCustomResponse,
PaginatedCustomResponseDto,
} from "@/types/pagination";
import { prepareUrl } from "@/utils/url";
import { coreApiClient } from "./_client";
import { dtoToIssue, issueQueryParamsToDto } from "./_transformers";
import { getAllLanguages } from "./languages";

const ISSUES_PATH = "/issues";

export async function getIssues(
query: IssueQueryParams & PaginationQueryParams = DEFAULT_QUERY,
): Promise<PaginatedCustomResponse<Issue>> {
let allLanguages: string[] = [];
if (query?.technologies?.length) {
allLanguages = await getAllLanguages();
}

const queryDto = issueQueryParamsToDto(query, allLanguages);
const url = prepareUrl(`${ISSUES_PATH}`, queryDto);
const res =
await coreApiClient.get<PaginatedCustomResponseDto<IssueDto>>(url);

return {
totalCount: res.total_count ?? 0,
hasNextPage: res.has_next_page,
hasPreviousPage: res.has_previous_page,
data: res.data.map(dtoToIssue),
};
}

export default { getIssues };
13 changes: 13 additions & 0 deletions api/core/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import tags from "@/utils/tags";
import { coreApiClient } from "./_client";

const LANGUAGES_PATH = "/languages";

export async function getAllLanguages() {
const response = await coreApiClient.get<string[]>(LANGUAGES_PATH, {
tag: tags.languages,
});
return response;
}

export default { getAllLanguages };
60 changes: 60 additions & 0 deletions api/core/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { DEFAULT_BIG_PAGE_SIZE, DEFAULT_QUERY } from "@/data/fetch";
import {
PaginatedCustomResponse,
PaginationQueryParams,
} from "@/types/pagination";
import { IFilterOption } from "@/types/filters";
import { Project } from "@/types/project";
import tags from "@/utils/tags";
import { prepareUrl } from "@/utils/url";
import { coreApiClient } from "./_client";

type QueryParams = {
slugs?: string[];
categories?: string[];
stackLevels?: string[];
technologies?: string[];
};

const PROJECTS_PATH = "/projects";

async function getProjects(
query: QueryParams & PaginationQueryParams = DEFAULT_QUERY,
tag?: string,
) {
const url = prepareUrl(PROJECTS_PATH, query);
const nextTag = tag ?? tags.projects(query.slugs?.join("-") || "");

return coreApiClient.get<PaginatedCustomResponse<Project>>(url, {
tag: nextTag,
});
}

async function getAllProjectOptions() {
let offset = 0;
let hasMore = true;
let projects: IFilterOption[] = [];

while (hasMore) {
const paginationParams = {
offset,
limit: DEFAULT_BIG_PAGE_SIZE,
tag: "all-projects",
};
const response = await getProjects(paginationParams, tags.projectOptions);

projects = projects.concat(
response.data.map((project) => ({
value: project.slug,
label: project.name,
})),
);

hasMore = response.hasNextPage;
offset += DEFAULT_BIG_PAGE_SIZE;
}

return projects;
}

export default { getProjects, getAllProjectOptions };
35 changes: 35 additions & 0 deletions api/core/repositories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
PaginatedCustomResponse,
PaginationQueryParams,
} from "@/types/pagination";
import { Repository, RepositoryDto } from "@/types/repository";
import tags from "@/utils/tags";
import { prepareUrl } from "@/utils/url";
import { coreApiClient } from "./_client";
import { dtoToRepository } from "./_transformers";

type QueryParams = {
slugs?: string[];
names?: string[];
languageIds?: string[];
projectIds?: string[];
};

const REPOSITORIES_PATH = "/repositories";

async function getRepositories(query: QueryParams & PaginationQueryParams) {
const url = prepareUrl(REPOSITORIES_PATH, query);
const tag = tags.repositories(query.slugs?.join("-") || "");
return coreApiClient.get<PaginatedCustomResponse<Repository>>(url, { tag });
}

async function getRepositoryById(repositoryId: number): Promise<Repository> {
const url = `${REPOSITORIES_PATH}/${repositoryId}`;
const res = await coreApiClient.get<RepositoryDto>(url, {
tag: `repository-${repositoryId}`,
});

return dtoToRepository(res);
}

export default { getRepositories, getRepositoryById };
5 changes: 0 additions & 5 deletions app/api/revalidate/[tag]/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";
import { getTags } from "@/utils/cache";

export async function POST(
_: NextRequest,
{ params }: { params: { tag: string } },
) {
const tag = params.tag;

if (!getTags().includes(tag)) {
return NextResponse.json({ error: "Invalid tag" }, { status: 400 });
}
revalidateTag(tag);

return NextResponse.json({ revalidated: tag });
Expand Down
66 changes: 66 additions & 0 deletions app/carnival/_components/EventBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import NextImage from "next/image";
import { IconRepo, IconSocial, IconWeb } from "@/assets/icons";
import Countdown from "@/components/countdown";

const EventBanner = () => (
<article className="rounded-xl p-6 bg-container-1 border-container-stroke-separator relative overflow-hidden h-[675px]">
<NextImage
className="pointer-events-none absolute -left-[5px] -top-[30px] h-[calc(100%_+_60px)] w-[calc(100%_+_10px)] max-w-[initial] object-cover object-top"
src="/images/kudos-weeks.png"
alt="Event banner"
height={983}
width={1200}
/>
<div className="relative flex flex-col gap-6 w-6/12">
<div className="flex flex-col gap-1">
<span className="text-xl text-primary italic">
{"kudos < > PBA host"}
</span>
<span className="tracking-tight text-6xl font-bold">
Kudos Carnival
</span>
<span className="text-xl text-default-600">
November 1, 2024 - December 15, 2024
</span>
</div>

<Countdown date="2024-11-01T12:00:00.536328Z" />

<h3 className="text-xl text-default-600 mt-4">
Want to level up your contributions? Make an impact on Polkadot, solve
key issues, and rise up the Kudos leaderboard!
</h3>
<div className="gap-3 flex flex-col sm:flex-row">
<div className="flex min-w-40 flex-col gap-2 rounded-xl bg-default text-default-foreground p-3">
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-text-1 flex items-center gap-2">
<IconSocial size={16} /> Participants
</span>
</div>
<span className="text-2xl font-bold text-text-1">TBA</span>
</div>
<div className="flex min-w-40 flex-col gap-2 rounded-xl bg-default text-default-foreground p-3">
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-text-1 flex items-center gap-2">
<IconRepo size={16} /> Issues to challenge
</span>
</div>
<span className="text-2xl font-bold text-text-1">
TBA
{/* 87 <span className="text-default-600 font-medium">/ 134</span> */}
</span>
</div>
<div className="flex min-w-40 flex-col gap-2 rounded-xl bg-default text-default-foreground p-3">
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-text-1 flex items-center gap-2">
<IconWeb size={16} /> Projects
</span>
</div>
<span className="text-2xl font-bold text-text-1">TBA</span>
</div>
</div>
</div>
</article>
);

export default EventBanner;
Loading

0 comments on commit cb356f5

Please sign in to comment.