Merge remote-tracking branch 'origin/monorepo' into intermediate-merge
This commit is contained in:
@@ -72,3 +72,6 @@ jobs:
|
||||
|
||||
- name: Lint (knip)
|
||||
run: pnpm run lint:knip
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
@@ -29,6 +29,9 @@ apps/frontend/.env
|
||||
apps/frontend/src/backend
|
||||
apps/frontend/build
|
||||
|
||||
build-ts
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/*
|
||||
|
||||
Vendored
+3
-2
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"markdown.extension.toc.levels": "1..3",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "prettier.prettier-vscode",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript]": {
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
},
|
||||
"cSpell.words": ["Wakatime"]
|
||||
}
|
||||
|
||||
@@ -161,11 +161,13 @@ const statsFetcher = async ({
|
||||
const repoNodesWithStars = repoNodes.filter(
|
||||
(node) => node.stargazers.totalCount !== 0,
|
||||
);
|
||||
|
||||
hasNextPage =
|
||||
(process.env.FETCH_MULTI_PAGE_STARS === "true" ||
|
||||
process.env.FETCH_MULTI_PAGE_STARS > fetchedPages) &&
|
||||
repoNodes.length === repoNodesWithStars.length &&
|
||||
res.data.data.user.repositories.pageInfo.hasNextPage;
|
||||
|
||||
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,29 +2,33 @@
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "2.11.2",
|
||||
"axios": "^1",
|
||||
"axios-cache-interceptor": "^1",
|
||||
"daisyui": "2.31.0",
|
||||
"emoji-name-map": "^2.0.3",
|
||||
"github-username-regex": "^1.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"react-redux": "9.2.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-toastify": "^9.1.3",
|
||||
"redux": "^4.2.1",
|
||||
"redux": "5.0.1",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"uuid": "^9.0.1",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.3.27",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/uuid": "9.0.8",
|
||||
"@vitejs/plugin-react-swc": "4.2.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"clsx": "2.1.1",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"vite": "7.3.1",
|
||||
@@ -34,7 +38,8 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build-trends": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"homepage": "/frontend",
|
||||
"browserslist": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { authenticate, getUserMetadata, deleteAccount } from "./user";
|
||||
|
||||
export { authenticate, getUserMetadata, deleteAccount };
|
||||
@@ -2,10 +2,16 @@ import axios from "axios";
|
||||
|
||||
import { HOST } from "../constants";
|
||||
|
||||
const authenticate = async (code, privateAccess, userKey) => {
|
||||
const authenticate = async (
|
||||
code: string,
|
||||
privateAccess: boolean,
|
||||
userKey: string,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const fullUrl = `https://${HOST}/api/authenticate?code=${code}&private_access=${privateAccess}&user_key=${userKey}`;
|
||||
const result = await axios.post(fullUrl);
|
||||
const result = await axios.post<{ userId: string; needDowngrade: boolean }>(
|
||||
fullUrl,
|
||||
);
|
||||
const { userId, needDowngrade } = result.data;
|
||||
if (needDowngrade) {
|
||||
console.info(
|
||||
@@ -20,10 +26,17 @@ const authenticate = async (code, privateAccess, userKey) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getUserMetadata = async (userKey) => {
|
||||
interface UserMetaDataResponse {
|
||||
token: string;
|
||||
privateAccess: string;
|
||||
}
|
||||
|
||||
const getUserMetadata = async (
|
||||
userKey: string,
|
||||
): Promise<null | UserMetaDataResponse> => {
|
||||
try {
|
||||
const fullUrl = `https://${HOST}/api/user-access?user_key=${userKey}`;
|
||||
const result = await axios.get(fullUrl);
|
||||
const result = await axios.get<UserMetaDataResponse>(fullUrl);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -31,7 +44,10 @@ const getUserMetadata = async (userKey) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAccount = async (userId, userKey) => {
|
||||
const deleteAccount = async (
|
||||
_userId: string,
|
||||
userKey: string,
|
||||
): Promise<unknown> => {
|
||||
try {
|
||||
const fullUrl = `https://${HOST}/api/delete-user?user_key=${userKey}`;
|
||||
const result = await axios.get(fullUrl);
|
||||
@@ -28,11 +28,21 @@ const cachedAxios = setupCache(axios, {
|
||||
axios.get = cachedAxios.get.bind(cachedAxios);
|
||||
axios.post = cachedAxios.post.bind(cachedAxios);
|
||||
|
||||
export function clearAxiosCache() {
|
||||
cachedAxios.storage.clear();
|
||||
export function clearAxiosCache(): void {
|
||||
void cachedAxios.storage.clear?.();
|
||||
}
|
||||
|
||||
function createMockResponse(data, config) {
|
||||
function createMockResponse<TData, TConfig>(
|
||||
data: TData,
|
||||
config: TConfig,
|
||||
): Promise<{
|
||||
data: TData;
|
||||
status: 200;
|
||||
statusText: string;
|
||||
headers: Record<string, never>;
|
||||
request: Record<string, never>;
|
||||
config: TConfig;
|
||||
}> {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
status: 200,
|
||||
@@ -44,9 +54,9 @@ function createMockResponse(data, config) {
|
||||
}
|
||||
|
||||
// store shouldMock outside React context so the interceptor can access it
|
||||
let shouldMock = null;
|
||||
let shouldMock = false;
|
||||
|
||||
export function setShouldMock(newShouldMock) {
|
||||
export function setShouldMock(newShouldMock: boolean): void {
|
||||
shouldMock = newShouldMock;
|
||||
}
|
||||
|
||||
@@ -58,7 +68,17 @@ axios.defaults.adapter = async (config) => {
|
||||
return defaultAdapter(config);
|
||||
}
|
||||
|
||||
const params = config.data ? JSON.parse(config.data) : {};
|
||||
interface Params {
|
||||
query?: string;
|
||||
variables?: {
|
||||
login?: string;
|
||||
repo?: string;
|
||||
gistName?: string;
|
||||
};
|
||||
}
|
||||
const params = (
|
||||
config.data ? JSON.parse(config.data as string) : {}
|
||||
) as Params;
|
||||
|
||||
if (
|
||||
config.url === "https://api.github.com/graphql" &&
|
||||
@@ -93,8 +113,9 @@ axios.defaults.adapter = async (config) => {
|
||||
if (
|
||||
config.url === "https://api.github.com/graphql" &&
|
||||
params.query?.includes("fragment RepoInfo on Repository {") &&
|
||||
params.variables?.login === "anuraghazra" &&
|
||||
params.variables?.repo === "github-readme-stats"
|
||||
params.variables &&
|
||||
params.variables.login === "anuraghazra" &&
|
||||
params.variables.repo === "github-readme-stats"
|
||||
) {
|
||||
return createMockResponse(repository, config);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import SVG from "./SVG";
|
||||
import { classnames } from "../../utils";
|
||||
import { HOST } from "../../constants";
|
||||
|
||||
export const Image = ({ imageSrc, stage, compact, extraClasses = "" }) => {
|
||||
const fullImageSrc = `https://${HOST}/api${imageSrc}&client=wizard`;
|
||||
|
||||
return (
|
||||
<div className={`${extraClasses} relative w-full relative`}>
|
||||
<SVG
|
||||
className="object-cover"
|
||||
url={fullImageSrc}
|
||||
compact={compact}
|
||||
stage={stage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Image.propTypes = {
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
stage: PropTypes.number.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
extraClasses: PropTypes.string,
|
||||
};
|
||||
|
||||
Image.defaultProps = {
|
||||
compact: false,
|
||||
extraClasses: "",
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
title,
|
||||
description,
|
||||
imageSrc,
|
||||
stage,
|
||||
selected,
|
||||
compact,
|
||||
fixedSize,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
"p-6 rounded border-2",
|
||||
fixedSize ? "h-[370px] w-[510px]" : "",
|
||||
selected
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 bg-white hover:bg-gray-50",
|
||||
)}
|
||||
>
|
||||
<h2 className="text-xl font-medium title-font text-gray-900">{title}</h2>
|
||||
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
|
||||
<Image
|
||||
imageSrc={imageSrc}
|
||||
compact={compact}
|
||||
extraClasses={fixedSize ? "flex justify-center" : ""}
|
||||
stage={stage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
stage: PropTypes.number.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
compact: PropTypes.bool,
|
||||
fixedSize: PropTypes.string,
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
selected: false,
|
||||
compact: false,
|
||||
fixedSize: false,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { CardImage } from "./CardImage";
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
imageSrc: string;
|
||||
stage: number;
|
||||
selected?: boolean;
|
||||
compact?: boolean;
|
||||
fixedSize?: boolean;
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
title,
|
||||
description,
|
||||
imageSrc,
|
||||
stage,
|
||||
selected = false,
|
||||
compact = false,
|
||||
fixedSize = false,
|
||||
}: CardProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("p-6 rounded border-2", {
|
||||
"h-[370px] w-[510px]": fixedSize,
|
||||
"border-blue-500 bg-blue-50": selected,
|
||||
"border-gray-200 bg-white hover:bg-gray-50": !selected,
|
||||
})}
|
||||
>
|
||||
<h2 className="text-xl font-medium title-font text-gray-900">{title}</h2>
|
||||
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
|
||||
<CardImage
|
||||
imageSrc={imageSrc}
|
||||
compact={compact}
|
||||
className={clsx({ "flex justify-center": fixedSize })}
|
||||
stage={stage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { HOST } from "../../constants";
|
||||
|
||||
import { SvgInline } from "./SvgInline";
|
||||
|
||||
interface CardImageProps {
|
||||
imageSrc: string;
|
||||
stage: number;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CardImage = ({
|
||||
imageSrc,
|
||||
stage,
|
||||
compact = false,
|
||||
className,
|
||||
}: CardImageProps) => {
|
||||
const fullImageSrc = `https://${HOST}/api${imageSrc}&client=wizard`;
|
||||
|
||||
return (
|
||||
<div className={clsx("relative w-full relative", className)}>
|
||||
<SvgInline
|
||||
className="object-cover"
|
||||
url={fullImageSrc}
|
||||
compact={compact}
|
||||
stage={stage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+42
-41
@@ -1,40 +1,55 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { JSX } from "react";
|
||||
import axios from "axios";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
import { createMockReq, createMockRes } from "../../mock-http";
|
||||
import { createMockRequest, createMockResponse } from "../../mock-http.js";
|
||||
// @ts-expect-error will be solved by npm package
|
||||
import { default as router } from "../../backend/.vercel/output/functions/api.func/router.js";
|
||||
import { setShouldMock } from "../../axios-override";
|
||||
import { setShouldMock } from "../../axios-override.js";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
useUserToken,
|
||||
} from "../../redux/selectors/userSelectors";
|
||||
import axios from "axios";
|
||||
} from "../../redux/selectors/userSelectors.js";
|
||||
|
||||
const SvgInline = (props) => {
|
||||
const [svg, setSvg] = useState(null);
|
||||
interface SvgInlineProps {
|
||||
url: string;
|
||||
stage: number;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
forceLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SvgInline(props: SvgInlineProps): JSX.Element {
|
||||
const {
|
||||
url,
|
||||
stage,
|
||||
className,
|
||||
compact = false,
|
||||
forceLoading = false,
|
||||
} = props;
|
||||
|
||||
const [svg, setSvg] = useState<string | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const containerRef = useRef(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const userToken = useUserToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { url, stage } = props;
|
||||
|
||||
// provide shouldMock to non-react code in axios-override.js
|
||||
useEffect(() => {
|
||||
setShouldMock(stage === 0 || !isAuthenticated);
|
||||
}, [isAuthenticated, props.stage]);
|
||||
}, [isAuthenticated, stage]);
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true;
|
||||
|
||||
const loadSvg = async () => {
|
||||
process.env.PAT_1 = userToken;
|
||||
window.process.env.PAT_1 = userToken as string;
|
||||
|
||||
setLoaded(false);
|
||||
|
||||
let body;
|
||||
let body: string;
|
||||
let status;
|
||||
|
||||
if (isAuthenticated && (!userToken || userToken === "placeholderPAT")) {
|
||||
@@ -43,17 +58,19 @@ const SvgInline = (props) => {
|
||||
}
|
||||
|
||||
if (stage === 4 && !isAuthenticated) {
|
||||
let res = await axios.get(url);
|
||||
const res = await axios.get<string>(url);
|
||||
body = res.data;
|
||||
status = res.status;
|
||||
} else {
|
||||
const req = createMockReq({
|
||||
const req = createMockRequest({
|
||||
method: "GET",
|
||||
url: url,
|
||||
url,
|
||||
});
|
||||
const res = createMockRes();
|
||||
const res = createMockResponse();
|
||||
// will be solved by npm package
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await router(req, res);
|
||||
body = res._getBody();
|
||||
body = res._getBody() as string;
|
||||
status = res._getStatusCode();
|
||||
}
|
||||
|
||||
@@ -68,12 +85,12 @@ const SvgInline = (props) => {
|
||||
setSvg(body);
|
||||
setLoaded(true);
|
||||
};
|
||||
loadSvg();
|
||||
void loadSvg();
|
||||
|
||||
return () => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [userToken, isAuthenticated, props.url, props.stage]);
|
||||
}, [userToken, isAuthenticated, url, stage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded && svg && containerRef.current) {
|
||||
@@ -91,8 +108,8 @@ const SvgInline = (props) => {
|
||||
}
|
||||
}, [loaded, svg]);
|
||||
|
||||
if (props.forceLoading || !loaded) {
|
||||
if (props.compact) {
|
||||
if (forceLoading || !loaded) {
|
||||
if (compact) {
|
||||
return <Skeleton style={{ paddingBottom: "58%" }} />;
|
||||
}
|
||||
// maximum dimensions of cards in SelectCard stage
|
||||
@@ -100,21 +117,5 @@ const SvgInline = (props) => {
|
||||
}
|
||||
|
||||
// Render a container div for the shadow DOM
|
||||
return <div ref={containerRef} id="svgWrapper" className={props.className} />;
|
||||
};
|
||||
|
||||
SvgInline.propTypes = {
|
||||
className: PropTypes.any,
|
||||
url: PropTypes.string.isRequired,
|
||||
forceLoading: PropTypes.bool,
|
||||
compact: PropTypes.bool,
|
||||
stage: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
SvgInline.defaultProps = {
|
||||
className: "",
|
||||
forceLoading: false,
|
||||
compact: false,
|
||||
};
|
||||
|
||||
export default SvgInline;
|
||||
return <div ref={containerRef} id="svgWrapper" className={className} />;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { classnames } from "../../utils";
|
||||
|
||||
const Button = (props) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...props}
|
||||
className={classnames(
|
||||
props.className,
|
||||
"border-0 py-2 px-6 inline-flex focus:outline-none rounded-[0.25rem] text-lg",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
export { Button };
|
||||
@@ -0,0 +1,22 @@
|
||||
import clsx from "clsx";
|
||||
import type { HTMLProps, JSX, ReactNode } from "react";
|
||||
|
||||
interface ButtonProps extends HTMLProps<HTMLButtonElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps): JSX.Element {
|
||||
const { className, children, ...rest } = props;
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"border-0 py-2 px-6 inline-flex focus:outline-none rounded-[0.25rem] text-lg",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Checkbox = ({ question, variable, setVariable, disabled }) => {
|
||||
return (
|
||||
<div
|
||||
className="flex inline-row mt-4"
|
||||
onClick={() => setVariable(!variable)}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={variable && !disabled ? "checked" : ""}
|
||||
className="checkbox mr-2"
|
||||
onChange={() => setVariable(!variable)}
|
||||
/>
|
||||
{question}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
question: PropTypes.string.isRequired,
|
||||
variable: PropTypes.bool.isRequired,
|
||||
setVariable: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
Checkbox.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export { Checkbox };
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { JSX, ReactNode } from "react";
|
||||
|
||||
interface CheckboxProps {
|
||||
question: ReactNode;
|
||||
checked: boolean;
|
||||
onCheckedChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
question,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled = false,
|
||||
}: CheckboxProps): JSX.Element {
|
||||
return (
|
||||
<label className="flex inline-row mt-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
className="checkbox mr-2"
|
||||
onChange={() => {
|
||||
onCheckedChange(!checked);
|
||||
}}
|
||||
/>
|
||||
{question}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { classnames } from "../../utils";
|
||||
|
||||
// options is of form [{value: '', label: '', disabled: true/false}]
|
||||
const Input = ({
|
||||
options,
|
||||
selectedOption,
|
||||
setSelectedOption,
|
||||
disabled,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<select
|
||||
className={classnames(
|
||||
"text-gray-700 bg-white select select-sm w-40 rounded-sm mt-4",
|
||||
className,
|
||||
)}
|
||||
value={selectedOption.label}
|
||||
onChange={(e) => {
|
||||
setSelectedOption(
|
||||
options.find((item) => item.label === e.target.value),
|
||||
);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
disabled={option.disabled}
|
||||
className={classnames(
|
||||
option.label === selectedOption.label && "bg-blue-200",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
Input.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
).isRequired,
|
||||
selectedOption: PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
setSelectedOption: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Input.defaultProps = {
|
||||
disabled: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,53 @@
|
||||
import clsx from "clsx";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface SelectOption {
|
||||
id: number;
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
options: Array<SelectOption>;
|
||||
selectedOption: SelectOption;
|
||||
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
onOptionChange: (option: SelectOption) => void;
|
||||
}
|
||||
|
||||
export function Select({
|
||||
options,
|
||||
selectedOption,
|
||||
onOptionChange,
|
||||
disabled,
|
||||
className,
|
||||
}: SelectProps): JSX.Element {
|
||||
return (
|
||||
<select
|
||||
className={clsx(
|
||||
"text-gray-700 bg-white select select-sm w-40 rounded-sm mt-4",
|
||||
className,
|
||||
)}
|
||||
value={selectedOption.value}
|
||||
onChange={(e) => {
|
||||
onOptionChange(options[e.target.selectedIndex] as SelectOption);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
disabled={option.disabled}
|
||||
className={clsx({
|
||||
"bg-blue-200": option.value === selectedOption.value,
|
||||
})}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Checkbox } from "../Generic/Checkbox";
|
||||
|
||||
const CheckboxSection = ({
|
||||
title,
|
||||
text,
|
||||
question,
|
||||
variable,
|
||||
setVariable,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<Section title={title}>
|
||||
{text && <p dangerouslySetInnerHTML={{ __html: text }} />}
|
||||
<Checkbox
|
||||
question={question}
|
||||
variable={variable}
|
||||
setVariable={setVariable}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
CheckboxSection.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
question: PropTypes.string.isRequired,
|
||||
variable: PropTypes.bool.isRequired,
|
||||
setVariable: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
CheckboxSection.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export { CheckboxSection };
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Section } from "./Section";
|
||||
import { Checkbox } from "../Generic/Checkbox";
|
||||
import type { JSX } from "react";
|
||||
|
||||
interface CheckboxSectionProps {
|
||||
title: string;
|
||||
question: string;
|
||||
checked: boolean;
|
||||
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
onCheckedChange: (check: boolean) => void;
|
||||
}
|
||||
|
||||
export function CheckboxSection({
|
||||
title,
|
||||
text,
|
||||
question,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled = false,
|
||||
}: CheckboxSectionProps): JSX.Element {
|
||||
return (
|
||||
<Section title={title}>
|
||||
{text && <p>{text}</p>}
|
||||
<Checkbox
|
||||
question={question}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Input } from "../Generic/Input";
|
||||
|
||||
export const DEFAULT_OPTION = {
|
||||
id: 1,
|
||||
label: "Normal",
|
||||
disabled: false,
|
||||
value: "normal",
|
||||
};
|
||||
|
||||
const LanguagesLayoutSection = ({ selectedOption, setSelectedOption }) => {
|
||||
const options = [
|
||||
DEFAULT_OPTION,
|
||||
{ id: 2, label: "Compact", disabled: false, value: "compact" },
|
||||
{ id: 3, label: "Donut", disabled: false, value: "donut" },
|
||||
{
|
||||
id: 4,
|
||||
label: "Vertical Donut",
|
||||
disabled: false,
|
||||
value: "donut-vertical",
|
||||
},
|
||||
{ id: 5, label: "Pie", disabled: false, value: "pie" },
|
||||
{
|
||||
id: 6,
|
||||
label: "Only Languages",
|
||||
disabled: false,
|
||||
value: "compact&hide_progress=true",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section title="Card Layout">
|
||||
<p>Select a card layout.</p>
|
||||
<Input
|
||||
options={options}
|
||||
selectedOption={selectedOption || DEFAULT_OPTION}
|
||||
setSelectedOption={setSelectedOption}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
LanguagesLayoutSection.propTypes = {
|
||||
selectedOption: PropTypes.object.isRequired,
|
||||
setSelectedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { LanguagesLayoutSection };
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Select } from "../Generic/Select";
|
||||
import type { SelectOption } from "../Generic/Select";
|
||||
|
||||
export const DEFAULT_OPTION: SelectOption = {
|
||||
id: 1,
|
||||
label: "Normal",
|
||||
disabled: false,
|
||||
value: "normal",
|
||||
};
|
||||
|
||||
const options: Array<SelectOption> = [
|
||||
DEFAULT_OPTION,
|
||||
{
|
||||
id: 2,
|
||||
label: "Compact",
|
||||
disabled: false,
|
||||
value: "compact",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "Donut",
|
||||
disabled: false,
|
||||
value: "donut",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: "Vertical Donut",
|
||||
disabled: false,
|
||||
value: "donut-vertical",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: "Pie",
|
||||
disabled: false,
|
||||
value: "pie",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: "Only Languages",
|
||||
disabled: false,
|
||||
value: "compact&hide_progress=true",
|
||||
},
|
||||
];
|
||||
|
||||
interface LanguagesLayoutSectionProps {
|
||||
selectedLanguageLayoutOption: SelectOption;
|
||||
onLanguageLayoutOptionChange: (option: SelectOption) => void;
|
||||
}
|
||||
|
||||
export function LanguagesLayoutSection({
|
||||
selectedLanguageLayoutOption,
|
||||
onLanguageLayoutOptionChange,
|
||||
}: LanguagesLayoutSectionProps): JSX.Element {
|
||||
return (
|
||||
<Section title="Card Layout">
|
||||
<p>Select a card layout.</p>
|
||||
<Select
|
||||
options={options}
|
||||
selectedOption={selectedLanguageLayoutOption}
|
||||
onOptionChange={onLanguageLayoutOptionChange}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
|
||||
const NumericSection = ({
|
||||
title,
|
||||
text,
|
||||
value,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
disabled,
|
||||
placeholder,
|
||||
}) => {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const debounceTimeout = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce setValue
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
if (internalValue === value) {
|
||||
return undefined;
|
||||
}
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
setValue(internalValue);
|
||||
}, 700);
|
||||
return () => clearTimeout(debounceTimeout.current);
|
||||
}, [internalValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||
<input
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-2 py-1 mt-2 w-1/4"
|
||||
value={internalValue}
|
||||
onChange={(e) => setInternalValue(e.target.value)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
NumericSection.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
value: PropTypes.number,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
step: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
};
|
||||
|
||||
NumericSection.defaultProps = {
|
||||
value: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
placeholder: "",
|
||||
};
|
||||
|
||||
export { NumericSection };
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { JSX, ReactNode } from "react";
|
||||
|
||||
import { Section } from "./Section";
|
||||
|
||||
interface NumericSectionProps {
|
||||
title: string;
|
||||
description: ReactNode;
|
||||
value?: number | undefined;
|
||||
onValueChange: (value: number | undefined) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function NumericSection({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onValueChange,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
}: NumericSectionProps): JSX.Element {
|
||||
const [internalValue, setInternalValue] = useState(() => value?.toString());
|
||||
const debounceTimeout = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce onValueChange
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
if (internalValue === value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
debounceTimeout.current = window.setTimeout(() => {
|
||||
const maybeNumber = internalValue && parseInt(internalValue, 10);
|
||||
if (typeof maybeNumber !== "number" || Number.isNaN(maybeNumber)) {
|
||||
onValueChange(undefined);
|
||||
} else {
|
||||
onValueChange(maybeNumber);
|
||||
}
|
||||
}, 700);
|
||||
|
||||
return () => {
|
||||
clearTimeout(debounceTimeout.current as number);
|
||||
};
|
||||
}, [internalValue, onValueChange, value]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value?.toString());
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<p>{description}</p>
|
||||
<input
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-2 py-1 mt-2 w-1/4"
|
||||
value={internalValue ?? ""}
|
||||
onChange={(e) => {
|
||||
setInternalValue(e.target.value);
|
||||
}}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import {
|
||||
FaArrowLeft as LeftArrowIcon,
|
||||
FaArrowRight as RightArrowIcon,
|
||||
} from "react-icons/fa";
|
||||
|
||||
import { classnames } from "../../utils";
|
||||
|
||||
const ProgressSection = ({ num, item, passed, isActive, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
className={classnames(
|
||||
"w-1/4 flex flex-col mx-2 p-2 cursor-pointer",
|
||||
passed ? "border-blue-500" : "border-gray-500",
|
||||
isActive ? "border-t-[14px] -mt-[5px]" : "border-t-4",
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
"text-lg font-bold",
|
||||
passed ? "text-blue-500" : "text-gray-500",
|
||||
isActive ? "-mt-[4px]" : "",
|
||||
)}
|
||||
>
|
||||
{`Step ${num + 1}`}
|
||||
</div>
|
||||
<div className={classnames(passed ? "text-gray-700" : "text-gray-500")}>
|
||||
{item}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressSection.propTypes = {
|
||||
num: PropTypes.number.isRequired,
|
||||
item: PropTypes.string.isRequired,
|
||||
passed: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProgressBar = ({ items, currItem, setCurrItem }) => {
|
||||
const leftDisabled = currItem === 0;
|
||||
const rightDisabled = currItem === items.length - 1;
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center sticky top-0 bg-gray-200 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
|
||||
<LeftArrowIcon
|
||||
className={classnames(
|
||||
"w-8 h-8",
|
||||
leftDisabled
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-700 cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrItem(Math.max(currItem - 1, 0))}
|
||||
/>
|
||||
<div className="px-2 flex-grow flex flex-row">
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<ProgressSection
|
||||
num={index}
|
||||
key={index}
|
||||
item={item}
|
||||
passed={currItem >= index}
|
||||
isActive={currItem === index}
|
||||
onClick={() => setCurrItem(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<RightArrowIcon
|
||||
className={classnames(
|
||||
"w-8 h-8",
|
||||
rightDisabled
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-700 cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrItem(Math.min(currItem + 1, items.length - 1))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressBar.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
currItem: PropTypes.number.isRequired,
|
||||
setCurrItem: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { ProgressBar };
|
||||
@@ -0,0 +1,100 @@
|
||||
import clsx from "clsx";
|
||||
import type { JSX, MouseEventHandler } from "react";
|
||||
|
||||
import {
|
||||
FaArrowLeft as LeftArrowIcon,
|
||||
FaArrowRight as RightArrowIcon,
|
||||
} from "react-icons/fa";
|
||||
|
||||
interface ProgressSectionProps {
|
||||
num: number;
|
||||
item: string;
|
||||
passed: boolean;
|
||||
isActive: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function ProgressSection({
|
||||
num,
|
||||
item,
|
||||
passed,
|
||||
isActive,
|
||||
onClick,
|
||||
}: ProgressSectionProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"w-1/4 flex flex-col mx-2 p-2 cursor-pointer",
|
||||
passed ? "border-blue-500" : "border-gray-500",
|
||||
isActive ? "border-t-[14px] -mt-[5px]" : "border-t-4",
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={clsx("text-lg font-bold", {
|
||||
"text-blue-500": passed,
|
||||
"text-gray-500": !passed,
|
||||
"-mt-[4px]": isActive,
|
||||
})}
|
||||
>
|
||||
{`Step ${num + 1}`}
|
||||
</div>
|
||||
<div className={passed ? "text-gray-700" : "text-gray-500"}>{item}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressBarProps {
|
||||
items: Array<string>;
|
||||
currItemIndex: number;
|
||||
onItemClick: (itemIndex: number) => void;
|
||||
}
|
||||
|
||||
export function ProgressBar({
|
||||
items,
|
||||
currItemIndex,
|
||||
onItemClick,
|
||||
}: ProgressBarProps): JSX.Element {
|
||||
const leftDisabled = currItemIndex === 0;
|
||||
const rightDisabled = currItemIndex === items.length - 1;
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center sticky top-0 bg-gray-200 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
|
||||
<LeftArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-gray-400 cursor-not-allowed": leftDisabled,
|
||||
"text-gray-700 cursor-pointer": !leftDisabled,
|
||||
})}
|
||||
onClick={() => {
|
||||
onItemClick(Math.max(currItemIndex - 1, 0));
|
||||
}}
|
||||
/>
|
||||
<div className="px-2 flex-grow flex flex-row">
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<ProgressSection
|
||||
num={index}
|
||||
key={item} // each step should have a unique name
|
||||
item={item}
|
||||
passed={currItemIndex >= index}
|
||||
isActive={currItemIndex === index}
|
||||
onClick={() => {
|
||||
onItemClick(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<RightArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-gray-400 cursor-not-allowed": rightDisabled,
|
||||
"text-gray-700 cursor-pointer": !rightDisabled,
|
||||
})}
|
||||
onClick={() => {
|
||||
onItemClick(Math.min(currItemIndex + 1, items.length - 1));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-18
@@ -1,9 +1,13 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import type { JSX, ReactNode } from "react";
|
||||
|
||||
import { HiOutlineLightningBolt as LightningIcon } from "react-icons/hi";
|
||||
|
||||
const Section = (props) => {
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Section({ title, children }: SectionProps): JSX.Element {
|
||||
return (
|
||||
<div className="flex relative pb-12">
|
||||
<div className="h-full w-10 absolute inset-0 flex items-center justify-center">
|
||||
@@ -15,22 +19,10 @@ const Section = (props) => {
|
||||
|
||||
<div className="flex-grow pl-4">
|
||||
<h2 className="font-medium title-font text-sm text-gray-900 mb-1 tracking-wider">
|
||||
{props.title}
|
||||
{title}
|
||||
</h2>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Section.propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
Section.defaultProps = {
|
||||
title: "Test",
|
||||
children: <p className="leading-relaxed">This is a test!</p>,
|
||||
};
|
||||
|
||||
export { Section };
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Input } from "../Generic/Input";
|
||||
|
||||
export const DEFAULT_OPTION = {
|
||||
id: 1,
|
||||
label: "Rank",
|
||||
disabled: false,
|
||||
value: "default",
|
||||
};
|
||||
|
||||
const StatsRankSection = ({ selectedOption, setSelectedOption }) => {
|
||||
const options = [
|
||||
DEFAULT_OPTION,
|
||||
{ id: 2, label: "Percentile", disabled: false, value: "percentile" },
|
||||
{ id: 3, label: "GitHub", disabled: false, value: "github" },
|
||||
{ id: 4, label: "None", disabled: false, value: "default&hide_rank=true" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Section title="Progress Style">
|
||||
<p>Select a progress style.</p>
|
||||
<Input
|
||||
options={options}
|
||||
selectedOption={selectedOption || DEFAULT_OPTION}
|
||||
setSelectedOption={setSelectedOption}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
StatsRankSection.propTypes = {
|
||||
selectedOption: PropTypes.object.isRequired,
|
||||
setSelectedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { StatsRankSection };
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { Select } from "../Generic/Select";
|
||||
import type { SelectOption } from "../Generic/Select";
|
||||
|
||||
import { Section } from "./Section";
|
||||
|
||||
export const DEFAULT_OPTION: SelectOption = {
|
||||
id: 1,
|
||||
label: "Rank",
|
||||
value: "default",
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const options: Array<SelectOption> = [
|
||||
DEFAULT_OPTION,
|
||||
{ id: 2, label: "Percentile", value: "percentile", disabled: false },
|
||||
{ id: 3, label: "GitHub", value: "github", disabled: false },
|
||||
{ id: 4, label: "None", value: "default&hide_rank=true", disabled: false },
|
||||
];
|
||||
|
||||
interface StatsRankSectionProps {
|
||||
selectedOption: SelectOption;
|
||||
onOptionChange: (option: SelectOption) => void;
|
||||
}
|
||||
|
||||
export function StatsRankSection({
|
||||
selectedOption,
|
||||
onOptionChange,
|
||||
}: StatsRankSectionProps): JSX.Element {
|
||||
return (
|
||||
<Section title="Progress Style">
|
||||
<p>Select a progress style.</p>
|
||||
<Select
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onOptionChange={onOptionChange}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { classnames } from "../../utils";
|
||||
|
||||
const TextSection = ({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
setValue,
|
||||
disabled,
|
||||
placeholder,
|
||||
onPaste,
|
||||
}) => {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const debounceTimeout = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce setValue
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
if (internalValue === value) {
|
||||
return undefined;
|
||||
}
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
setValue(internalValue);
|
||||
}, 700);
|
||||
// return cleanup function:
|
||||
return () => clearTimeout(debounceTimeout.current);
|
||||
}, [internalValue]);
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<p>{description}</p>
|
||||
<input
|
||||
type="text"
|
||||
className={classnames(
|
||||
"border border-gray-300 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
|
||||
disabled ? "cursor-not-allowed" : "",
|
||||
)}
|
||||
value={internalValue}
|
||||
onChange={(e) => setInternalValue(e.target.value)}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
TextSection.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.node.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onPaste: PropTypes.func,
|
||||
};
|
||||
|
||||
TextSection.defaultProps = {
|
||||
disabled: false,
|
||||
placeholder: "",
|
||||
onPaste: undefined,
|
||||
};
|
||||
|
||||
export { TextSection };
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ClipboardEventHandler, JSX, ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Section } from "./Section";
|
||||
|
||||
interface TextSectionProps {
|
||||
title: string;
|
||||
description: ReactNode;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
onPaste?: ClipboardEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export function TextSection({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
onPaste,
|
||||
}: TextSectionProps): JSX.Element {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const debounceTimeout = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce onValueChange
|
||||
if (debounceTimeout.current) {
|
||||
window.clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
if (internalValue === value) {
|
||||
return undefined;
|
||||
}
|
||||
debounceTimeout.current = window.setTimeout(() => {
|
||||
onValueChange(internalValue);
|
||||
}, 700);
|
||||
// return cleanup function:
|
||||
return () => {
|
||||
window.clearTimeout(debounceTimeout.current as number);
|
||||
};
|
||||
}, [internalValue, onValueChange, value]);
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<p>{description}</p>
|
||||
<input
|
||||
type="text"
|
||||
className={clsx(
|
||||
"border border-gray-300 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
|
||||
{ "cursor-not-allowed": disabled },
|
||||
)}
|
||||
value={internalValue}
|
||||
onChange={(e) => {
|
||||
setInternalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Input } from "../Generic/Input";
|
||||
|
||||
export const DEFAULT_OPTION = {
|
||||
id: 1,
|
||||
label: "Normal",
|
||||
disabled: false,
|
||||
value: "default",
|
||||
};
|
||||
|
||||
const WakatimeLayoutSection = ({ selectedOption, setSelectedOption }) => {
|
||||
const options = [
|
||||
DEFAULT_OPTION,
|
||||
{ id: 2, label: "Compact", disabled: false, value: "compact" },
|
||||
{
|
||||
id: 3,
|
||||
label: "Text Only",
|
||||
disabled: false,
|
||||
value: "default&hide_progress=true&card_width=315",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section title="Card Layout">
|
||||
<p>Select a card layout.</p>
|
||||
<Input
|
||||
options={options}
|
||||
selectedOption={selectedOption || DEFAULT_OPTION}
|
||||
setSelectedOption={setSelectedOption}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
WakatimeLayoutSection.propTypes = {
|
||||
selectedOption: PropTypes.object.isRequired,
|
||||
setSelectedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WakatimeLayoutSection;
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { Section } from "./Section";
|
||||
import { Select } from "../Generic/Select";
|
||||
import type { SelectOption } from "../Generic/Select";
|
||||
|
||||
export const DEFAULT_OPTION: SelectOption = {
|
||||
id: 1,
|
||||
label: "Normal",
|
||||
disabled: false,
|
||||
value: "default",
|
||||
};
|
||||
|
||||
const options: Array<SelectOption> = [
|
||||
DEFAULT_OPTION,
|
||||
{
|
||||
id: 2,
|
||||
label: "Compact",
|
||||
disabled: false,
|
||||
value: "compact",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "Text Only",
|
||||
disabled: false,
|
||||
value: "default&hide_progress=true&card_width=315",
|
||||
},
|
||||
];
|
||||
|
||||
interface WakatimeLayoutSectionProps {
|
||||
selectedOption: SelectOption;
|
||||
onOptionChange: (option: SelectOption) => void;
|
||||
}
|
||||
|
||||
export function WakatimeLayoutSection({
|
||||
selectedOption,
|
||||
onOptionChange,
|
||||
}: WakatimeLayoutSectionProps): JSX.Element {
|
||||
return (
|
||||
<Section title="Card Layout">
|
||||
<p>Select a card layout.</p>
|
||||
<Select
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onOptionChange={onOptionChange}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const PROD = false;
|
||||
const PROD = false as boolean;
|
||||
|
||||
export const USE_LOGGER = true;
|
||||
export const USE_LOGGER = true as boolean;
|
||||
|
||||
export const CLIENT_ID = "Ov23lilAc5biyyRY0K1u";
|
||||
|
||||
@@ -20,7 +20,7 @@ export const DEMO_WAKATIME_USER = "ffflabs";
|
||||
|
||||
window.process = {
|
||||
env: {
|
||||
FETCH_MULTI_PAGE_STARS: 10,
|
||||
FETCH_MULTI_PAGE_STARS: "10",
|
||||
PAT_1: "placeholderPAT", // so the backend's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly
|
||||
},
|
||||
};
|
||||
} as (typeof window)["process"];
|
||||
@@ -1,4 +0,0 @@
|
||||
// Safe browser stub for dotenv
|
||||
export function config() {
|
||||
return { parsed: {} };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Safe browser stub for dotenv
|
||||
export function config(): { parsed: Record<string, never> } {
|
||||
return { parsed: {} };
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
declare global {
|
||||
interface CustomProcess {
|
||||
env: {
|
||||
FETCH_MULTI_PAGE_STARS: string | undefined;
|
||||
PAT_1: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
interface Window {
|
||||
process?: CustomProcess;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,13 +1,11 @@
|
||||
import "./axios-override";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import configureStore from "./redux/store";
|
||||
import { AppTrends } from "./pages/App";
|
||||
import "./index.css";
|
||||
import { store } from "./redux/store";
|
||||
import { AppTrends } from "./pages/App/AppTrends";
|
||||
|
||||
export const store = configureStore();
|
||||
import "./index.css";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
export function createMockReq({
|
||||
method = "GET",
|
||||
url,
|
||||
headers = {},
|
||||
body = null,
|
||||
} = {}) {
|
||||
return {
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockRes() {
|
||||
let statusCode = 200;
|
||||
const headers = {};
|
||||
let chunks = [];
|
||||
|
||||
const res = {
|
||||
statusCode,
|
||||
chunks,
|
||||
|
||||
setHeader(name, value) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
},
|
||||
getHeader(name) {
|
||||
return headers[name.toLowerCase()];
|
||||
},
|
||||
getHeaders() {
|
||||
return { ...headers };
|
||||
},
|
||||
|
||||
write(chunk) {
|
||||
if (typeof chunk !== "string") {
|
||||
chunk = String(chunk);
|
||||
}
|
||||
chunks.push(chunk);
|
||||
},
|
||||
|
||||
end(chunk) {
|
||||
res.write(chunk);
|
||||
},
|
||||
|
||||
// --- Helpers for inspection ---
|
||||
_getStatusCode() {
|
||||
return statusCode;
|
||||
},
|
||||
_getHeaders() {
|
||||
return { ...headers };
|
||||
},
|
||||
_getBody() {
|
||||
return chunks.join("");
|
||||
},
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
type HeaderMap = Partial<Record<string, string>>;
|
||||
|
||||
type CreateMockRequestOptions = {
|
||||
url: string;
|
||||
headers?: HeaderMap;
|
||||
} & (
|
||||
| {
|
||||
method: "GET";
|
||||
}
|
||||
| {
|
||||
method: "POST";
|
||||
body: unknown;
|
||||
}
|
||||
);
|
||||
|
||||
type CreateMockRequestResult = {
|
||||
url: string;
|
||||
headers: HeaderMap;
|
||||
} & (
|
||||
| {
|
||||
method: "GET";
|
||||
}
|
||||
| {
|
||||
method: "POST";
|
||||
body: unknown;
|
||||
}
|
||||
);
|
||||
|
||||
export function createMockRequest(
|
||||
options: CreateMockRequestOptions,
|
||||
): CreateMockRequestResult {
|
||||
const { headers = {}, ...rest } = options;
|
||||
|
||||
return { ...rest, headers };
|
||||
}
|
||||
|
||||
interface CreateMockResponseResult {
|
||||
statusCode: number;
|
||||
chunks: Array<unknown>;
|
||||
|
||||
setHeader(name: string, value: string): void;
|
||||
getHeader(name: string): string | undefined;
|
||||
getHeaders(): HeaderMap;
|
||||
|
||||
write(chunk: unknown): void;
|
||||
end(chunk: unknown): void;
|
||||
|
||||
// --- Helpers for inspection ---
|
||||
_getStatusCode(): number;
|
||||
_getHeaders(): HeaderMap;
|
||||
_getBody(): unknown;
|
||||
}
|
||||
|
||||
export function createMockResponse(): CreateMockResponseResult {
|
||||
const statusCode = 200;
|
||||
const headers: HeaderMap = {};
|
||||
const chunks: Array<unknown> = [];
|
||||
|
||||
const res: CreateMockResponseResult = {
|
||||
statusCode,
|
||||
chunks,
|
||||
|
||||
setHeader(name, value) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
},
|
||||
getHeader(name) {
|
||||
return headers[name.toLowerCase()];
|
||||
},
|
||||
getHeaders() {
|
||||
return { ...headers };
|
||||
},
|
||||
|
||||
write(chunk) {
|
||||
if (typeof chunk !== "string") {
|
||||
chunk = String(chunk);
|
||||
}
|
||||
chunks.push(chunk);
|
||||
},
|
||||
|
||||
end(chunk) {
|
||||
res.write(chunk);
|
||||
},
|
||||
|
||||
_getStatusCode() {
|
||||
return statusCode;
|
||||
},
|
||||
_getHeaders() {
|
||||
return { ...headers };
|
||||
},
|
||||
_getBody() {
|
||||
return chunks.join("");
|
||||
},
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const CardType = {
|
||||
STATS: "stats",
|
||||
TOP_LANGS: "top-langs",
|
||||
PIN: "pin",
|
||||
GIST: "gist",
|
||||
WAKATIME: "wakatime",
|
||||
} as const;
|
||||
export type CardType = (typeof CardType)[keyof typeof CardType];
|
||||
@@ -0,0 +1,28 @@
|
||||
export const STAGE_LABELS = [
|
||||
{
|
||||
title: "Login",
|
||||
shortTitle: "Login",
|
||||
},
|
||||
{
|
||||
title: "Select a Card",
|
||||
shortTitle: "Select Card",
|
||||
},
|
||||
{
|
||||
title: "Modify Card Parameters",
|
||||
shortTitle: "Modify Parameters",
|
||||
},
|
||||
{
|
||||
title: "Choose a Theme",
|
||||
shortTitle: "Select Theme",
|
||||
},
|
||||
{
|
||||
title: "Display your Card",
|
||||
shortTitle: "Display Card",
|
||||
},
|
||||
] as const satisfies Array<{ title: string; shortTitle: string }>;
|
||||
|
||||
export type StageIndex = {
|
||||
[K in keyof typeof STAGE_LABELS]: K extends `${infer N extends number}`
|
||||
? N
|
||||
: never;
|
||||
}[keyof typeof STAGE_LABELS];
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
declare module "*.css";
|
||||
|
||||
declare module "*.png" {
|
||||
const img: string;
|
||||
export default img;
|
||||
}
|
||||
|
||||
// This package doesn't have a @types counter part
|
||||
declare module "save-svg-as-png" {
|
||||
const saveSvgAsPng: (
|
||||
element: Element,
|
||||
filename: string,
|
||||
options: {
|
||||
scale: number;
|
||||
encoderOptions: number;
|
||||
},
|
||||
) => void;
|
||||
export { saveSvgAsPng };
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import {
|
||||
logout as _logout,
|
||||
setUserAccess as _setUserAccess,
|
||||
} from "../../redux/actions/userActions";
|
||||
|
||||
import Header from "./Header";
|
||||
import HomeScreen from "../Home";
|
||||
import { getUserMetadata } from "../../api";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
useUserKey,
|
||||
useUserToken,
|
||||
} from "../../redux/selectors/userSelectors";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { clearAxiosCache } from "../../axios-override";
|
||||
|
||||
function App() {
|
||||
const toMessage = (input) => {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input.reason?.message) {
|
||||
return input.reason.message;
|
||||
}
|
||||
if (input.message) {
|
||||
return input.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(input);
|
||||
} catch {
|
||||
return "Unknown error";
|
||||
}
|
||||
};
|
||||
|
||||
const showError = (event) => {
|
||||
toast.error(toMessage(event), {
|
||||
position: "bottom-right",
|
||||
autoClose: 1500,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("error", (event) => {
|
||||
showError(event);
|
||||
});
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
showError(event);
|
||||
});
|
||||
|
||||
const userToken = useUserToken();
|
||||
useEffect(() => {
|
||||
clearAxiosCache();
|
||||
}, [userToken]);
|
||||
|
||||
const userKey = useUserKey();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const [stage, setStage] = useState(isAuthenticated ? 1 : 0);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const setUserAccess = (access) =>
|
||||
dispatch(_setUserAccess(access.token, access.privateAccess));
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && stage === 0) {
|
||||
setStage(1);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getPrivateAccess() {
|
||||
if (userKey && userKey.length > 0) {
|
||||
const userAccess = await getUserMetadata(userKey);
|
||||
if (userAccess === null) {
|
||||
dispatch(_logout(userKey));
|
||||
} else {
|
||||
setUserAccess(userAccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
getPrivateAccess();
|
||||
}, [userKey]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Router basename="/frontend">
|
||||
<Header stage={stage} setStage={setStage} />
|
||||
<section className="bg-white text-gray-700 flex-grow">
|
||||
<HomeScreen stage={stage} setStage={setStage} />
|
||||
<ToastContainer />
|
||||
</section>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
import {
|
||||
logout as _logout,
|
||||
setUserAccess as _setUserAccess,
|
||||
} from "../../redux/slices/user";
|
||||
import { clearAxiosCache } from "../../axios-override";
|
||||
import { HomeScreen } from "../Home/Home";
|
||||
import { getUserMetadata } from "../../api/user";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
useUserKey,
|
||||
useUserToken,
|
||||
} from "../../redux/selectors/userSelectors";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
|
||||
import { Header } from "./Header";
|
||||
|
||||
const toMessage = (
|
||||
input: string | ErrorEvent | PromiseRejectionEvent,
|
||||
): string => {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
type MaybeErrorReason = { message: string } | null;
|
||||
const reason = ("reason" in input ? input.reason : null) as MaybeErrorReason;
|
||||
if (typeof reason?.message === "string" && !!reason.message.trim()) {
|
||||
return reason.message;
|
||||
}
|
||||
|
||||
if ("message" in input && input.message) {
|
||||
return input.message;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(input);
|
||||
} catch {
|
||||
return "Unknown error";
|
||||
}
|
||||
};
|
||||
|
||||
const showError = (event: ErrorEvent | PromiseRejectionEvent): void => {
|
||||
toast.error(toMessage(event), {
|
||||
position: "bottom-right",
|
||||
autoClose: 1500,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
});
|
||||
};
|
||||
|
||||
export function AppTrends() {
|
||||
const userKey = useUserKey();
|
||||
const userToken = useUserToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
const [stage, setStage] = useState<StageIndex>(isAuthenticated ? 1 : 0);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: ErrorEvent | PromiseRejectionEvent) => {
|
||||
showError(event);
|
||||
};
|
||||
|
||||
window.addEventListener("error", handler);
|
||||
window.addEventListener("unhandledrejection", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("error", handler);
|
||||
window.removeEventListener("unhandledrejection", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
clearAxiosCache();
|
||||
}, [userToken]);
|
||||
|
||||
{
|
||||
/**
|
||||
* This effect mus be executed only on page load,
|
||||
* otherwise logged in user are unable to go back on first step
|
||||
*/
|
||||
const hasCheckedUserAuthStatusOnLoad = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasCheckedUserAuthStatusOnLoad.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasCheckedUserAuthStatusOnLoad.current = true;
|
||||
|
||||
if (isAuthenticated && stage === 0) {
|
||||
setStage(1);
|
||||
}
|
||||
}, [isAuthenticated, stage]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function getPrivateAccess() {
|
||||
if (userKey && userKey.length > 0) {
|
||||
const userAccess = await getUserMetadata(userKey);
|
||||
if (userAccess === null) {
|
||||
dispatch(_logout({ userKey }));
|
||||
} else {
|
||||
dispatch(_setUserAccess(userAccess));
|
||||
}
|
||||
}
|
||||
}
|
||||
void getPrivateAccess();
|
||||
}, [dispatch, userKey]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header currStageIndex={stage} onStageIndexChange={setStage} />
|
||||
<section className="bg-white text-gray-700 flex-grow">
|
||||
<HomeScreen stage={stage} setStage={setStage} />
|
||||
<ToastContainer />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import appIcon from "../../assets/appLogo64.png";
|
||||
import { classnames } from "../../utils";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
import { ProgressBar } from "../../components/Home/Progress";
|
||||
|
||||
const propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onClick: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
const StandardLink = ({ to, children, onClick, className }) => (
|
||||
<Link
|
||||
to={to}
|
||||
className={classnames(
|
||||
"px-4 py-1 mr-3 rounded-sm bg-gray-200 hover:bg-gray-300 text-gray-700",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
StandardLink.propTypes = propTypes;
|
||||
|
||||
StandardLink.defaultProps = defaultProps;
|
||||
|
||||
const MobileLink = ({ to, children, onClick, className }) => (
|
||||
<Link
|
||||
to={to}
|
||||
className={classnames(
|
||||
"block text-sm px-2 my-2 py-2 rounded-sm bg-gray-200 text-gray-700",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
MobileLink.propTypes = propTypes;
|
||||
|
||||
MobileLink.defaultProps = defaultProps;
|
||||
|
||||
const Header = ({ stage, setStage }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-100 bg-gray-800 shadow-md body-font z-50">
|
||||
<div className="px-5 py-2 flex flex-wrap">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center title-font font-medium text-gray-50 mb-0 md:mr-8"
|
||||
>
|
||||
<img src={appIcon} alt="logo" className="w-6 h-6" />
|
||||
<span className="ml-2 text-xl">GitHub Stats Extended</span>
|
||||
</Link>
|
||||
{/* Star on GitHub */}
|
||||
<div className="flex ml-auto items-center text-base justify-center">
|
||||
<a
|
||||
href="https://github.com/stats-organization/github-stats-extended"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-[0.25rem] shadow bg-gray-200 hover:bg-gray-300 text-black px-3 py-1 flex items-center"
|
||||
>
|
||||
Star on
|
||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
items={[
|
||||
"Login",
|
||||
"Select Card",
|
||||
"Modify Parameters",
|
||||
"Select Theme",
|
||||
"Display Card",
|
||||
]}
|
||||
currItem={stage}
|
||||
setCurrItem={setStage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
stage: PropTypes.number.isRequired,
|
||||
setStage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import appIcon from "../../assets/appLogo64.png";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
import { ProgressBar } from "../../components/Home/Progress";
|
||||
import { STAGE_LABELS } from "../../models/Stage";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
|
||||
interface HeaderProps {
|
||||
currStageIndex: StageIndex;
|
||||
onStageIndexChange: (stageIndex: StageIndex) => void;
|
||||
}
|
||||
|
||||
const items = STAGE_LABELS.map((it) => it.shortTitle);
|
||||
|
||||
export function Header({
|
||||
currStageIndex,
|
||||
onStageIndexChange,
|
||||
}: HeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-100 bg-gray-800 shadow-md body-font z-50">
|
||||
<div className="px-5 py-2 flex flex-wrap">
|
||||
{/* Logo */}
|
||||
<a
|
||||
href="/"
|
||||
className="flex items-center title-font font-medium text-gray-50 mb-0 md:mr-8"
|
||||
>
|
||||
<img src={appIcon} alt="logo" className="w-6 h-6" />
|
||||
<span className="ml-2 text-xl">GitHub Stats Extended</span>
|
||||
</a>
|
||||
{/* Star on GitHub */}
|
||||
<div className="flex ml-auto items-center text-base justify-center">
|
||||
<a
|
||||
href="https://github.com/stats-organization/github-stats-extended"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-[0.25rem] shadow bg-gray-200 hover:bg-gray-300 text-black px-3 py-1 flex items-center"
|
||||
>
|
||||
Star on
|
||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
items={items}
|
||||
currItemIndex={currStageIndex}
|
||||
onItemClick={(itemIndex) => {
|
||||
onStageIndexChange(itemIndex as StageIndex);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import AppTrends from "./AppTrends";
|
||||
|
||||
export { AppTrends };
|
||||
@@ -1,18 +1,12 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import BounceLoader from "react-spinners/BounceLoader";
|
||||
|
||||
import axios from "axios";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { CustomizeStage } from "./stages/Customize";
|
||||
import { DisplayStage } from "./stages/Display";
|
||||
import { LoginStage } from "./stages/Login";
|
||||
import { SelectCardStage } from "./stages/SelectCard";
|
||||
import { ThemeStage } from "./stages/Theme";
|
||||
|
||||
import { authenticate } from "../../api";
|
||||
import { login as _login } from "../../redux/actions/userActions";
|
||||
import { authenticate } from "../../api/user";
|
||||
import { login } from "../../redux/slices/user";
|
||||
import {
|
||||
HOST,
|
||||
DEMO_USER,
|
||||
@@ -20,7 +14,7 @@ import {
|
||||
DEMO_REPO,
|
||||
DEMO_GIST,
|
||||
} from "../../constants";
|
||||
import { CardTypes } from "../../utils";
|
||||
import { CardType } from "../../models/CardType";
|
||||
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
|
||||
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
|
||||
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
|
||||
@@ -29,9 +23,21 @@ import {
|
||||
useIsAuthenticated,
|
||||
usePrivateAccess,
|
||||
} from "../../redux/selectors/userSelectors";
|
||||
import axios from "axios";
|
||||
import { STAGE_LABELS } from "../../models/Stage";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
|
||||
const HomeScreen = ({ stage, setStage }) => {
|
||||
import { CustomizeStage } from "./stages/Customize";
|
||||
import { DisplayStage } from "./stages/Display";
|
||||
import { LoginStage } from "./stages/Login/Login";
|
||||
import { SelectCardStage } from "./stages/SelectCard";
|
||||
import { ThemeStage } from "./stages/Theme";
|
||||
|
||||
interface HomeScreenProps {
|
||||
stage: StageIndex;
|
||||
setStage: (stageIndex: StageIndex) => void;
|
||||
}
|
||||
|
||||
export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const userId = useUserId(DEMO_USER);
|
||||
@@ -40,15 +46,13 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey));
|
||||
|
||||
// for stage two
|
||||
const [selectedUserId, setSelectedUserId] = useState(userId);
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>(userId);
|
||||
const [repo, setRepo] = useState(DEMO_REPO);
|
||||
const [gist, setGist] = useState(DEMO_GIST);
|
||||
const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
|
||||
|
||||
const [selectedCard, setSelectedCard] = useState("stats");
|
||||
const [selectedCard, setSelectedCard] = useState<CardType>(CardType.STATS);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedUserId(userId);
|
||||
@@ -66,9 +70,11 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
|
||||
const [showTitle, setShowTitle] = useState(true);
|
||||
const [showOwner, setShowOwner] = useState(false);
|
||||
const [descriptionLines, setDescriptionLines] = useState();
|
||||
const [descriptionLines, setDescriptionLines] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
const [customTitle, setCustomTitle] = useState("");
|
||||
const [langsCount, setLangsCount] = useState();
|
||||
const [langsCount, setLangsCount] = useState<number | undefined>();
|
||||
const [showAllStats, setShowAllStats] = useState(false);
|
||||
const [showIcons, setShowIcons] = useState(false);
|
||||
const [includeAllCommits, setIncludeAllCommits] = useState(true);
|
||||
@@ -77,95 +83,93 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
|
||||
const [theme, setTheme] = useState("default");
|
||||
|
||||
const resetCustomization = () => {
|
||||
if (selectedCard === CardTypes.TOP_LANGS) {
|
||||
const handleCardTypeChange = (cardType: CardType) => {
|
||||
if (cardType === CardType.TOP_LANGS) {
|
||||
setLangsCount(4);
|
||||
}
|
||||
if (selectedCard === CardTypes.WAKATIME) {
|
||||
setLangsCount(6);
|
||||
}
|
||||
|
||||
if (selectedCard === CardTypes.TOP_LANGS) {
|
||||
setSelectedWakatimeLayout(WAKATIME_DEFAULT_LAYOUT);
|
||||
}
|
||||
if (selectedCard === CardTypes.WAKATIME) {
|
||||
} else if (cardType === CardType.WAKATIME) {
|
||||
setLangsCount(6);
|
||||
setSelectedLanguagesLayout(LANGUAGES_DEFAULT_LAYOUT);
|
||||
}
|
||||
|
||||
if (theme === "default" || theme === "default_repocard") {
|
||||
if (selectedCard === CardTypes.PIN || selectedCard === CardTypes.GIST) {
|
||||
if (cardType === CardType.PIN || cardType === CardType.GIST) {
|
||||
setTheme("default_repocard");
|
||||
} else {
|
||||
setTheme("default");
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedCard(cardType);
|
||||
// Go to the next stage
|
||||
setStage(2);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetCustomization();
|
||||
}, [selectedCard]);
|
||||
|
||||
let fullSuffix = `${selectedCard === CardTypes.STATS ? "" : "/" + selectedCard}?`;
|
||||
let fullSuffix = `${selectedCard === CardType.STATS ? "" : "/" + selectedCard}?`;
|
||||
|
||||
switch (selectedCard) {
|
||||
case CardTypes.STATS:
|
||||
case CardTypes.TOP_LANGS:
|
||||
case CardType.STATS:
|
||||
case CardType.TOP_LANGS:
|
||||
fullSuffix += `username=${selectedUserId}`;
|
||||
break;
|
||||
case CardTypes.PIN:
|
||||
case CardType.PIN:
|
||||
fullSuffix += `username=${userId}&repo=${repo}`;
|
||||
break;
|
||||
case CardTypes.GIST:
|
||||
case CardType.GIST:
|
||||
fullSuffix += `id=${gist}`;
|
||||
break;
|
||||
case CardTypes.WAKATIME:
|
||||
case CardType.WAKATIME:
|
||||
fullSuffix += `username=${wakatimeUser}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
selectedCard satisfies never;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedStatsRank !== STATS_DEFAULT_RANK &&
|
||||
selectedCard === CardTypes.STATS
|
||||
selectedCard === CardType.STATS
|
||||
) {
|
||||
fullSuffix += `&rank_icon=${selectedStatsRank.value}`;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLanguagesLayout !== LANGUAGES_DEFAULT_LAYOUT &&
|
||||
selectedCard === CardTypes.TOP_LANGS
|
||||
selectedCard === CardType.TOP_LANGS
|
||||
) {
|
||||
fullSuffix += `&layout=${selectedLanguagesLayout.value}`;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedWakatimeLayout !== WAKATIME_DEFAULT_LAYOUT &&
|
||||
selectedCard === CardTypes.WAKATIME
|
||||
selectedCard === CardType.WAKATIME
|
||||
) {
|
||||
fullSuffix += `&layout=${selectedWakatimeLayout.value}`;
|
||||
}
|
||||
|
||||
if (
|
||||
!showTitle &&
|
||||
(selectedCard === CardTypes.STATS ||
|
||||
selectedCard === CardTypes.TOP_LANGS ||
|
||||
selectedCard === CardTypes.WAKATIME)
|
||||
(selectedCard === CardType.STATS ||
|
||||
selectedCard === CardType.TOP_LANGS ||
|
||||
selectedCard === CardType.WAKATIME)
|
||||
) {
|
||||
fullSuffix += "&hide_title=true";
|
||||
}
|
||||
|
||||
if (
|
||||
showOwner &&
|
||||
(selectedCard === CardTypes.PIN || selectedCard === CardTypes.GIST)
|
||||
(selectedCard === CardType.PIN || selectedCard === CardType.GIST)
|
||||
) {
|
||||
fullSuffix += "&show_owner=true";
|
||||
}
|
||||
|
||||
if (descriptionLines && selectedCard === CardTypes.PIN) {
|
||||
if (descriptionLines && selectedCard === CardType.PIN) {
|
||||
fullSuffix += `&description_lines_count=${descriptionLines}`;
|
||||
}
|
||||
|
||||
if (
|
||||
customTitle &&
|
||||
(selectedCard === CardTypes.STATS || selectedCard === CardTypes.WAKATIME)
|
||||
(selectedCard === CardType.STATS || selectedCard === CardType.WAKATIME)
|
||||
) {
|
||||
const encodedTitle = encodeURIComponent(customTitle);
|
||||
fullSuffix += `&custom_title=${encodedTitle}`;
|
||||
@@ -173,34 +177,33 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
|
||||
if (
|
||||
langsCount &&
|
||||
(selectedCard === CardTypes.TOP_LANGS ||
|
||||
selectedCard === CardTypes.WAKATIME)
|
||||
(selectedCard === CardType.TOP_LANGS || selectedCard === CardType.WAKATIME)
|
||||
) {
|
||||
fullSuffix += `&langs_count=${langsCount}`;
|
||||
}
|
||||
|
||||
if (showAllStats && selectedCard === CardTypes.STATS) {
|
||||
if (showAllStats && selectedCard === CardType.STATS) {
|
||||
fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`;
|
||||
}
|
||||
|
||||
if (showIcons && selectedCard === CardTypes.STATS) {
|
||||
if (showIcons && selectedCard === CardType.STATS) {
|
||||
fullSuffix += `&show_icons=true`;
|
||||
}
|
||||
|
||||
if (includeAllCommits && selectedCard === CardTypes.STATS) {
|
||||
if (includeAllCommits && selectedCard === CardType.STATS) {
|
||||
fullSuffix += `&include_all_commits=true`;
|
||||
}
|
||||
|
||||
if (
|
||||
!enableAnimations &&
|
||||
(selectedCard === CardTypes.STATS ||
|
||||
selectedCard === CardTypes.TOP_LANGS ||
|
||||
selectedCard === CardTypes.WAKATIME)
|
||||
(selectedCard === CardType.STATS ||
|
||||
selectedCard === CardType.TOP_LANGS ||
|
||||
selectedCard === CardType.WAKATIME)
|
||||
) {
|
||||
fullSuffix += `&disable_animations=${!enableAnimations}`;
|
||||
}
|
||||
|
||||
if (usePercent && selectedCard === CardTypes.WAKATIME) {
|
||||
if (usePercent && selectedCard === CardType.WAKATIME) {
|
||||
fullSuffix += `&display_format=percent`;
|
||||
}
|
||||
|
||||
@@ -210,11 +213,11 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
if (
|
||||
!(
|
||||
(theme === "default" &&
|
||||
[CardTypes.STATS, CardTypes.TOP_LANGS, CardTypes.WAKATIME].includes(
|
||||
selectedCard,
|
||||
[CardType.STATS, CardType.TOP_LANGS, CardType.WAKATIME].includes(
|
||||
selectedCard as never,
|
||||
)) ||
|
||||
(theme === "default_repocard" &&
|
||||
[CardTypes.PIN, CardTypes.GIST].includes(selectedCard))
|
||||
[CardType.PIN, CardType.GIST].includes(selectedCard as never))
|
||||
)
|
||||
) {
|
||||
themeSuffix += `&theme=${theme}`;
|
||||
@@ -223,38 +226,37 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
// for stage five
|
||||
const [gistUrl, setGistUrl] = useState("");
|
||||
|
||||
let guestHint;
|
||||
switch (selectedCard) {
|
||||
case CardTypes.STATS:
|
||||
case CardTypes.TOP_LANGS:
|
||||
guestHint = `username "${DEMO_USER}"`;
|
||||
break;
|
||||
case CardTypes.PIN:
|
||||
guestHint = `repo "${DEMO_REPO}"`;
|
||||
break;
|
||||
case CardTypes.GIST:
|
||||
guestHint = `Gist ID "${DEMO_GIST}"`;
|
||||
break;
|
||||
case CardTypes.WAKATIME:
|
||||
guestHint = `WakaTime username "${DEMO_WAKATIME_USER}"`;
|
||||
}
|
||||
const guestHint = useMemo(() => {
|
||||
switch (selectedCard) {
|
||||
case CardType.STATS:
|
||||
case CardType.TOP_LANGS:
|
||||
return `username "${DEMO_USER}"`;
|
||||
case CardType.PIN:
|
||||
return `repo "${DEMO_REPO}"`;
|
||||
case CardType.GIST:
|
||||
return `Gist ID "${DEMO_GIST}"`;
|
||||
case CardType.WAKATIME:
|
||||
return `WakaTime username "${DEMO_WAKATIME_USER}"`;
|
||||
default:
|
||||
selectedCard satisfies never;
|
||||
return "";
|
||||
}
|
||||
}, [selectedCard]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGistUrl = async (gistId) => {
|
||||
async function fetchGistURL() {
|
||||
try {
|
||||
const fullUrl = `https://api.github.com/gists/${gistId}`;
|
||||
const result = await axios.get(fullUrl);
|
||||
return result.data.html_url;
|
||||
const fullUrl = `https://api.github.com/gists/${gist}`;
|
||||
const result = await axios.get<{ html_url: string }>(fullUrl);
|
||||
setGistUrl(result.data.html_url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
fetchGistUrl(gist).then(setGistUrl);
|
||||
}
|
||||
void fetchGistURL();
|
||||
}, [gist]);
|
||||
|
||||
const contentSectionRef = useRef(null);
|
||||
const contentSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// scroll to top of content section if we're scrolled down
|
||||
@@ -276,9 +278,9 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
// If Github API returns the code parameter
|
||||
if (url.includes("code=")) {
|
||||
const tempPrivateAccess = url.includes("private");
|
||||
const newUrl = url.split("?code=");
|
||||
const redirect = `${url.split(HOST)[0]}${HOST}/frontend`;
|
||||
window.history.pushState({}, null, redirect);
|
||||
const newUrl = url.split("?code=", 2) as [string, string];
|
||||
const redirect = `${url.split(HOST)[0] as string}${HOST}/frontend`;
|
||||
window.history.pushState({}, "", redirect);
|
||||
setIsLoading(true);
|
||||
const userKey = uuidv4();
|
||||
const newUserId = await authenticate(
|
||||
@@ -286,13 +288,15 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
tempPrivateAccess,
|
||||
userKey,
|
||||
);
|
||||
login(newUserId, userKey);
|
||||
|
||||
dispatch(login({ userId: newUserId, userKey }));
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
redirectCode();
|
||||
}, []);
|
||||
void redirectCode();
|
||||
}, [dispatch]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -311,15 +315,7 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
<div className="m-4 rounded-sm">
|
||||
<div className="lg:p-4">
|
||||
<div className="text-2xl text-gray-600 font-semibold">
|
||||
{
|
||||
[
|
||||
"Login",
|
||||
"Select a Card",
|
||||
"Modify Card Parameters",
|
||||
"Choose a Theme",
|
||||
"Display your Card",
|
||||
][stage]
|
||||
}
|
||||
{STAGE_LABELS[stage].title}
|
||||
</div>
|
||||
<div>
|
||||
{stage === 0 && isAuthenticated ? (
|
||||
@@ -360,17 +356,22 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{stage === 0 && <LoginStage setCurrItem={setStage} />}
|
||||
{stage === 0 && (
|
||||
<LoginStage
|
||||
onContinueAsGuest={() => {
|
||||
setStage(1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{stage === 1 && (
|
||||
<SelectCardStage
|
||||
selectedCard={selectedCard}
|
||||
setSelectedCard={setSelectedCard}
|
||||
setStage={setStage}
|
||||
selectedCardType={selectedCard}
|
||||
onCardTypeChange={handleCardTypeChange}
|
||||
/>
|
||||
)}
|
||||
{stage === 2 && (
|
||||
<CustomizeStage
|
||||
selectedCard={selectedCard || CardTypes.STATS}
|
||||
selectedCard={selectedCard}
|
||||
selectedStatsRank={selectedStatsRank}
|
||||
setSelectedStatsRank={setSelectedStatsRank}
|
||||
selectedLanguagesLayout={selectedLanguagesLayout}
|
||||
@@ -411,46 +412,52 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
)}
|
||||
{stage === 3 && (
|
||||
<ThemeStage
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
setStage={setStage}
|
||||
fullSuffix={fullSuffix}
|
||||
theme={theme}
|
||||
onThemeChange={(theme) => {
|
||||
setTheme(theme);
|
||||
setStage(4);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{stage === 4 && (
|
||||
<DisplayStage
|
||||
// eslint-disable-next-line consistent-return
|
||||
filename={(() => {
|
||||
switch (selectedCard) {
|
||||
case CardTypes.STATS:
|
||||
case CardTypes.TOP_LANGS:
|
||||
case CardType.STATS:
|
||||
case CardType.TOP_LANGS:
|
||||
return `${selectedUserId}_card`;
|
||||
case CardTypes.PIN:
|
||||
case CardType.PIN:
|
||||
return `${repo}_card`;
|
||||
case CardTypes.GIST:
|
||||
case CardType.GIST:
|
||||
return `gist_card`;
|
||||
case CardTypes.WAKATIME:
|
||||
case CardType.WAKATIME:
|
||||
return `${wakatimeUser}_card`;
|
||||
default:
|
||||
selectedCard satisfies never;
|
||||
return "";
|
||||
}
|
||||
})()}
|
||||
// eslint-disable-next-line consistent-return
|
||||
link={(() => {
|
||||
switch (selectedCard) {
|
||||
case CardTypes.STATS:
|
||||
case CardTypes.TOP_LANGS:
|
||||
case CardType.STATS:
|
||||
case CardType.TOP_LANGS:
|
||||
return `https://${HOST}/api${themeSuffix}`;
|
||||
|
||||
case CardTypes.PIN: {
|
||||
case CardType.PIN: {
|
||||
let myRepo = repo;
|
||||
if (!myRepo.includes("/")) {
|
||||
myRepo = `${userId}/${myRepo}`;
|
||||
}
|
||||
return `https://github.com/${myRepo}`;
|
||||
}
|
||||
case CardTypes.GIST:
|
||||
case CardType.GIST:
|
||||
return gistUrl;
|
||||
case CardTypes.WAKATIME:
|
||||
case CardType.WAKATIME:
|
||||
return `https://wakatime.com/@${wakatimeUser}`;
|
||||
default:
|
||||
selectedCard satisfies never;
|
||||
return "";
|
||||
}
|
||||
})()}
|
||||
themeSuffix={themeSuffix}
|
||||
@@ -465,11 +472,4 @@ const HomeScreen = ({ stage, setStage }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HomeScreen.propTypes = {
|
||||
stage: PropTypes.number.isRequired,
|
||||
setStage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HomeScreen;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import HomeScreen from "./Home";
|
||||
|
||||
export default HomeScreen;
|
||||
+120
-104
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import type React from "react";
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { CardTypes } from "../../../utils";
|
||||
import { Image } from "../../../components/Card/Card";
|
||||
import { CardType } from "../../../models/CardType";
|
||||
import { CardImage } from "../../../components/Card/CardImage";
|
||||
import { CheckboxSection } from "../../../components/Home/CheckboxSection";
|
||||
import { TextSection } from "../../../components/Home/TextSection";
|
||||
import { NumericSection } from "../../../components/Home/NumericSection";
|
||||
import { StatsRankSection } from "../../../components/Home/StatsRankSection";
|
||||
import { LanguagesLayoutSection } from "../../../components/Home/LanguagesLayoutSection";
|
||||
import WakatimeLayoutSection from "../../../components/Home/WakatimeLayoutSection";
|
||||
import { WakatimeLayoutSection } from "../../../components/Home/WakatimeLayoutSection";
|
||||
import {
|
||||
DEMO_GIST,
|
||||
DEMO_REPO,
|
||||
@@ -16,8 +16,53 @@ import {
|
||||
DEMO_WAKATIME_USER,
|
||||
} from "../../../constants";
|
||||
import { useIsAuthenticated } from "../../../redux/selectors/userSelectors";
|
||||
import type { SelectOption } from "../../../components/Generic/Select";
|
||||
import type { StageIndex } from "../../../models/Stage";
|
||||
|
||||
const CustomizeStage = ({
|
||||
type Updater<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||
|
||||
/** @todo todo consider using React context API to avoid prop drilling */
|
||||
interface CustomizeStageProps {
|
||||
selectedCard: CardType;
|
||||
selectedStatsRank: SelectOption;
|
||||
setSelectedStatsRank: Updater<SelectOption>;
|
||||
selectedLanguagesLayout: SelectOption;
|
||||
setSelectedLanguagesLayout: Updater<SelectOption>;
|
||||
selectedWakatimeLayout: SelectOption;
|
||||
setSelectedWakatimeLayout: Updater<SelectOption>;
|
||||
selectedUserId: string;
|
||||
setSelectedUserId: Updater<string>;
|
||||
repo: string;
|
||||
setRepo: Updater<string>;
|
||||
gist: string;
|
||||
setGist: Updater<string>;
|
||||
wakatimeUser: string;
|
||||
setWakatimeUser: Updater<string>;
|
||||
showTitle: boolean;
|
||||
setShowTitle: Updater<boolean>;
|
||||
descriptionLines: number | undefined;
|
||||
setDescriptionLines: Updater<number | undefined>;
|
||||
showOwner: boolean;
|
||||
setShowOwner: Updater<boolean>;
|
||||
customTitle: string;
|
||||
setCustomTitle: Updater<string>;
|
||||
langsCount: number | undefined;
|
||||
setLangsCount: Updater<number | undefined>;
|
||||
showIcons: boolean;
|
||||
setShowIcons: Updater<boolean>;
|
||||
showAllStats: boolean;
|
||||
setShowAllStats: Updater<boolean>;
|
||||
includeAllCommits: boolean;
|
||||
setIncludeAllCommits: Updater<boolean>;
|
||||
enableAnimations: boolean;
|
||||
setEnableAnimations: Updater<boolean>;
|
||||
usePercent: boolean;
|
||||
setUsePercent: Updater<boolean>;
|
||||
fullSuffix: string;
|
||||
setStage: (stageIndex: StageIndex) => void;
|
||||
}
|
||||
|
||||
export function CustomizeStage({
|
||||
selectedCard,
|
||||
selectedStatsRank,
|
||||
setSelectedStatsRank,
|
||||
@@ -55,14 +100,14 @@ const CustomizeStage = ({
|
||||
setUsePercent,
|
||||
fullSuffix,
|
||||
setStage,
|
||||
}) => {
|
||||
const cardType = selectedCard || CardTypes.STATS;
|
||||
}: CustomizeStageProps): JSX.Element {
|
||||
const cardType = selectedCard;
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-wrap">
|
||||
<div className="h-auto lg:w-2/5 md:w-1/2 p-10 rounded-sm bg-gray-200">
|
||||
{(cardType === CardTypes.STATS || cardType === CardTypes.TOP_LANGS) && (
|
||||
{(cardType === CardType.STATS || cardType === CardType.TOP_LANGS) && (
|
||||
<TextSection
|
||||
title="Username"
|
||||
description={
|
||||
@@ -89,7 +134,7 @@ const CustomizeStage = ({
|
||||
}
|
||||
placeholder={`e.g. "${DEMO_USER}"`}
|
||||
value={selectedUserId}
|
||||
setValue={setSelectedUserId}
|
||||
onValueChange={setSelectedUserId}
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
let newValue = e.clipboardData.getData("text");
|
||||
@@ -97,7 +142,7 @@ const CustomizeStage = ({
|
||||
if (newValue.endsWith("/")) {
|
||||
newValue = newValue.slice(0, -1);
|
||||
}
|
||||
let parts = newValue.split("/");
|
||||
const parts = newValue.split("/");
|
||||
if (parts.length > 1) {
|
||||
newValue = parts.slice(-1).join("/");
|
||||
}
|
||||
@@ -106,7 +151,7 @@ const CustomizeStage = ({
|
||||
disabled={!isAuthenticated}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.PIN && (
|
||||
{cardType === CardType.PIN && (
|
||||
<TextSection
|
||||
title="Repository"
|
||||
description={
|
||||
@@ -133,7 +178,7 @@ const CustomizeStage = ({
|
||||
}
|
||||
placeholder={`e.g. "${DEMO_REPO}"`}
|
||||
value={repo}
|
||||
setValue={setRepo}
|
||||
onValueChange={setRepo}
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
let newValue = e.clipboardData.getData("text");
|
||||
@@ -141,7 +186,7 @@ const CustomizeStage = ({
|
||||
if (newValue.endsWith("/")) {
|
||||
newValue = newValue.slice(0, -1);
|
||||
}
|
||||
let parts = newValue.split("/");
|
||||
const parts = newValue.split("/");
|
||||
if (parts.length > 2) {
|
||||
newValue = parts.slice(-2).join("/");
|
||||
}
|
||||
@@ -150,7 +195,7 @@ const CustomizeStage = ({
|
||||
disabled={!isAuthenticated}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.GIST && (
|
||||
{cardType === CardType.GIST && (
|
||||
<TextSection
|
||||
title="Repository"
|
||||
description={
|
||||
@@ -177,7 +222,7 @@ const CustomizeStage = ({
|
||||
}
|
||||
placeholder={`e.g. "${DEMO_GIST}"`}
|
||||
value={gist}
|
||||
setValue={setGist}
|
||||
onValueChange={setGist}
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
let newValue = e.clipboardData.getData("text");
|
||||
@@ -185,7 +230,7 @@ const CustomizeStage = ({
|
||||
if (newValue.endsWith("/")) {
|
||||
newValue = newValue.slice(0, -1);
|
||||
}
|
||||
let parts = newValue.split("/");
|
||||
const parts = newValue.split("/");
|
||||
if (parts.length > 1) {
|
||||
newValue = parts.slice(-1).join("/");
|
||||
}
|
||||
@@ -194,7 +239,7 @@ const CustomizeStage = ({
|
||||
disabled={!isAuthenticated}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.WAKATIME && (
|
||||
{cardType === CardType.WAKATIME && (
|
||||
<TextSection
|
||||
title="WakaTime Username"
|
||||
description={
|
||||
@@ -212,86 +257,92 @@ const CustomizeStage = ({
|
||||
}
|
||||
placeholder={`e.g. "${DEMO_WAKATIME_USER}"`}
|
||||
value={wakatimeUser}
|
||||
setValue={setWakatimeUser}
|
||||
onValueChange={setWakatimeUser}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.STATS && (
|
||||
{cardType === CardType.STATS && (
|
||||
<CheckboxSection
|
||||
title="Show All Stats?"
|
||||
text="Show all available statistics."
|
||||
question="Show all stats?"
|
||||
variable={showAllStats}
|
||||
setVariable={setShowAllStats}
|
||||
checked={showAllStats}
|
||||
onCheckedChange={setShowAllStats}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.STATS && (
|
||||
{cardType === CardType.STATS && (
|
||||
<StatsRankSection
|
||||
selectedOption={selectedStatsRank}
|
||||
setSelectedOption={setSelectedStatsRank}
|
||||
onOptionChange={setSelectedStatsRank}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.STATS && (
|
||||
{cardType === CardType.STATS && (
|
||||
<CheckboxSection
|
||||
title="Show Icons?"
|
||||
text="Show icons next to all stats."
|
||||
question="Show icons?"
|
||||
variable={showIcons}
|
||||
setVariable={setShowIcons}
|
||||
checked={showIcons}
|
||||
onCheckedChange={setShowIcons}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.STATS && (
|
||||
{cardType === CardType.STATS && (
|
||||
<CheckboxSection
|
||||
title="Include All Commits?"
|
||||
text="Count total commits or just commits of the last 365 days."
|
||||
question="Include all commits?"
|
||||
variable={includeAllCommits}
|
||||
setVariable={setIncludeAllCommits}
|
||||
checked={includeAllCommits}
|
||||
onCheckedChange={setIncludeAllCommits}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.TOP_LANGS && (
|
||||
{cardType === CardType.TOP_LANGS && (
|
||||
<LanguagesLayoutSection
|
||||
selectedOption={selectedLanguagesLayout}
|
||||
setSelectedOption={setSelectedLanguagesLayout}
|
||||
selectedLanguageLayoutOption={selectedLanguagesLayout}
|
||||
onLanguageLayoutOptionChange={setSelectedLanguagesLayout}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.WAKATIME && (
|
||||
{cardType === CardType.WAKATIME && (
|
||||
<WakatimeLayoutSection
|
||||
selectedOption={selectedWakatimeLayout}
|
||||
setSelectedOption={setSelectedWakatimeLayout}
|
||||
onOptionChange={setSelectedWakatimeLayout}
|
||||
/>
|
||||
)}
|
||||
{(cardType === CardTypes.TOP_LANGS ||
|
||||
cardType === CardTypes.WAKATIME) && (
|
||||
{(cardType === CardType.TOP_LANGS ||
|
||||
cardType === CardType.WAKATIME) && (
|
||||
<NumericSection
|
||||
title="Language Count"
|
||||
text="Set the number of languages to be shown.<br>Leave empty for default count."
|
||||
description={
|
||||
<>
|
||||
Set the number of languages to be shown.
|
||||
<br />
|
||||
Leave empty for default count.
|
||||
</>
|
||||
}
|
||||
value={langsCount}
|
||||
setValue={setLangsCount}
|
||||
onValueChange={setLangsCount}
|
||||
min={1}
|
||||
max={20}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.WAKATIME && (
|
||||
{cardType === CardType.WAKATIME && (
|
||||
<CheckboxSection
|
||||
title="Show Percentages?"
|
||||
text="Show time spent in hours or percentages."
|
||||
question="Show percentages?"
|
||||
variable={usePercent}
|
||||
setVariable={setUsePercent}
|
||||
checked={usePercent}
|
||||
onCheckedChange={setUsePercent}
|
||||
/>
|
||||
)}
|
||||
{(cardType === CardTypes.STATS ||
|
||||
cardType === CardTypes.TOP_LANGS ||
|
||||
cardType === CardTypes.WAKATIME) && (
|
||||
{(cardType === CardType.STATS ||
|
||||
cardType === CardType.TOP_LANGS ||
|
||||
cardType === CardType.WAKATIME) && (
|
||||
<CheckboxSection
|
||||
title="Show Title?"
|
||||
text="Shows a title at the top of the card."
|
||||
question="Show title?"
|
||||
variable={showTitle}
|
||||
setVariable={setShowTitle}
|
||||
checked={showTitle}
|
||||
onCheckedChange={setShowTitle}
|
||||
/>
|
||||
)}
|
||||
{(cardType === CardTypes.STATS || cardType === CardTypes.WAKATIME) && (
|
||||
{(cardType === CardType.STATS || cardType === CardType.WAKATIME) && (
|
||||
<TextSection
|
||||
title="Custom Title"
|
||||
description={
|
||||
@@ -303,35 +354,42 @@ const CustomizeStage = ({
|
||||
}
|
||||
placeholder='e.g. "My GitHub Stats"'
|
||||
value={customTitle}
|
||||
setValue={setCustomTitle}
|
||||
onValueChange={setCustomTitle}
|
||||
/>
|
||||
)}
|
||||
{(cardType === CardTypes.STATS ||
|
||||
cardType === CardTypes.TOP_LANGS ||
|
||||
cardType === CardTypes.WAKATIME) && (
|
||||
{(cardType === CardType.STATS ||
|
||||
cardType === CardType.TOP_LANGS ||
|
||||
cardType === CardType.WAKATIME) && (
|
||||
<CheckboxSection
|
||||
title="Enable Animations?"
|
||||
// text="Enable Animations."
|
||||
question="enable animations?"
|
||||
variable={enableAnimations}
|
||||
setVariable={setEnableAnimations}
|
||||
checked={enableAnimations}
|
||||
onCheckedChange={setEnableAnimations}
|
||||
/>
|
||||
)}
|
||||
{(cardType === CardTypes.PIN || cardType === CardTypes.GIST) && (
|
||||
{(cardType === CardType.PIN || cardType === CardType.GIST) && (
|
||||
<CheckboxSection
|
||||
title="Show Owner?"
|
||||
text="Shows the repo owner's name next to the repo name."
|
||||
question="Show owner?"
|
||||
variable={showOwner}
|
||||
setVariable={setShowOwner}
|
||||
checked={showOwner}
|
||||
onCheckedChange={setShowOwner}
|
||||
/>
|
||||
)}
|
||||
{cardType === CardTypes.PIN && (
|
||||
{cardType === CardType.PIN && (
|
||||
<NumericSection
|
||||
title="Description Lines Count"
|
||||
text="Set the number of lines for the description. Will be clamped between 1 and 3.<br>Leave empty for automatic adjustment."
|
||||
description={
|
||||
<>
|
||||
Set the number of lines for the description. Will be clamped
|
||||
between 1 and 3.
|
||||
<br />
|
||||
Leave empty for automatic adjustment.
|
||||
</>
|
||||
}
|
||||
value={descriptionLines}
|
||||
setValue={setDescriptionLines}
|
||||
onValueChange={setDescriptionLines}
|
||||
min={1}
|
||||
max={3}
|
||||
/>
|
||||
@@ -350,51 +408,9 @@ const CustomizeStage = ({
|
||||
</div>
|
||||
<div className="w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0">
|
||||
<div className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32">
|
||||
<Image imageSrc={fullSuffix} stage={2} />
|
||||
<CardImage imageSrc={fullSuffix} stage={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CustomizeStage.propTypes = {
|
||||
selectedCard: PropTypes.string.isRequired,
|
||||
selectedStatsRank: PropTypes.object.isRequired,
|
||||
setSelectedStatsRank: PropTypes.func.isRequired,
|
||||
selectedLanguagesLayout: PropTypes.object.isRequired,
|
||||
setSelectedLanguagesLayout: PropTypes.func.isRequired,
|
||||
selectedWakatimeLayout: PropTypes.object.isRequired,
|
||||
setSelectedWakatimeLayout: PropTypes.func.isRequired,
|
||||
selectedUserId: PropTypes.string.isRequired,
|
||||
setSelectedUserId: PropTypes.func.isRequired,
|
||||
repo: PropTypes.string.isRequired,
|
||||
setRepo: PropTypes.func.isRequired,
|
||||
gist: PropTypes.string.isRequired,
|
||||
setGist: PropTypes.func.isRequired,
|
||||
wakatimeUser: PropTypes.string.isRequired,
|
||||
setWakatimeUser: PropTypes.func.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
setShowTitle: PropTypes.func.isRequired,
|
||||
descriptionLines: PropTypes.number.isRequired,
|
||||
setDescriptionLines: PropTypes.func.isRequired,
|
||||
showOwner: PropTypes.bool.isRequired,
|
||||
setShowOwner: PropTypes.func.isRequired,
|
||||
customTitle: PropTypes.string.isRequired,
|
||||
setCustomTitle: PropTypes.func.isRequired,
|
||||
langsCount: PropTypes.number.isRequired,
|
||||
setLangsCount: PropTypes.func.isRequired,
|
||||
showIcons: PropTypes.bool.isRequired,
|
||||
setShowIcons: PropTypes.func.isRequired,
|
||||
showAllStats: PropTypes.bool.isRequired,
|
||||
setShowAllStats: PropTypes.func.isRequired,
|
||||
includeAllCommits: PropTypes.bool.isRequired,
|
||||
setIncludeAllCommits: PropTypes.func.isRequired,
|
||||
enableAnimations: PropTypes.bool.isRequired,
|
||||
setEnableAnimations: PropTypes.func.isRequired,
|
||||
usePercent: PropTypes.bool.isRequired,
|
||||
setUsePercent: PropTypes.func.isRequired,
|
||||
fullSuffix: PropTypes.string.isRequired,
|
||||
setStage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { CustomizeStage };
|
||||
}
|
||||
+40
-38
@@ -1,23 +1,30 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { saveSvgAsPng } from "save-svg-as-png";
|
||||
import { toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
import { saveSvgAsPng } from "save-svg-as-png";
|
||||
|
||||
import { Button } from "../../../components/Generic/Button";
|
||||
import { Image } from "../../../components/Card/Card";
|
||||
import { classnames } from "../../../utils";
|
||||
import { CardImage } from "../../../components/Card/CardImage";
|
||||
import { HOST } from "../../../constants";
|
||||
import type { JSX } from "react";
|
||||
|
||||
const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
interface DisplayStageProps {
|
||||
filename: string;
|
||||
link: string;
|
||||
themeSuffix: string;
|
||||
guestHint: string | null;
|
||||
}
|
||||
|
||||
export function DisplayStage({
|
||||
filename,
|
||||
link,
|
||||
themeSuffix,
|
||||
guestHint,
|
||||
}: DisplayStageProps): JSX.Element {
|
||||
const downloadPNG = () => {
|
||||
saveSvgAsPng(
|
||||
document.getElementById("svgWrapper").shadowRoot.firstElementChild
|
||||
.firstElementChild,
|
||||
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
|
||||
?.firstElementChild as HTMLElement,
|
||||
`${filename}.png`,
|
||||
{
|
||||
scale: 2,
|
||||
@@ -27,7 +34,7 @@ const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
};
|
||||
|
||||
const copyMarkdown = () => {
|
||||
navigator.clipboard.writeText(
|
||||
void navigator.clipboard.writeText(
|
||||
`[](${link})`,
|
||||
);
|
||||
toast.info("Copied to Clipboard!", {
|
||||
@@ -37,12 +44,11 @@ const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(`https://${HOST}/api${themeSuffix}`);
|
||||
void navigator.clipboard.writeText(`https://${HOST}/api${themeSuffix}`);
|
||||
toast.info("Copied to Clipboard!", {
|
||||
position: "bottom-right",
|
||||
autoClose: 1500,
|
||||
@@ -50,7 +56,6 @@ const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -65,29 +70,35 @@ const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
highlight: true,
|
||||
onClick: copyMarkdown,
|
||||
},
|
||||
{ title: "Copy URL", highlight: false, onClick: copyUrl },
|
||||
{ title: "Download PNG", highlight: false, onClick: downloadPNG },
|
||||
].map((item, index) => (
|
||||
{
|
||||
title: "Copy URL",
|
||||
highlight: false,
|
||||
onClick: copyUrl,
|
||||
},
|
||||
{
|
||||
title: "Download PNG",
|
||||
highlight: false,
|
||||
onClick: downloadPNG,
|
||||
},
|
||||
].map((item) => (
|
||||
<Button
|
||||
key={index}
|
||||
className={classnames(
|
||||
"m-4 w-60 flex justify-center",
|
||||
item.highlight
|
||||
? "bg-blue-500 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-100 text-black",
|
||||
)}
|
||||
key={item.title}
|
||||
className={clsx("m-4 w-60 flex justify-center", {
|
||||
"bg-blue-500 hover:bg-blue-600 text-white": item.highlight,
|
||||
"bg-white hover:bg-gray-100 text-black": !item.highlight,
|
||||
})}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.title}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{guestHint && <div className="pt-10 pl-10 pr-10">{guestHint}</div>}
|
||||
{!!guestHint && <div className="pt-10 pl-10 pr-10">{guestHint}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0">
|
||||
<div className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32">
|
||||
<Image
|
||||
<CardImage
|
||||
imageSrc={`${themeSuffix}&disable_animations=true`}
|
||||
stage={4}
|
||||
/>
|
||||
@@ -95,13 +106,4 @@ const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DisplayStage.propTypes = {
|
||||
filename: PropTypes.string.isRequired,
|
||||
link: PropTypes.string.isRequired,
|
||||
themeSuffix: PropTypes.string.isRequired,
|
||||
guestHint: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { DisplayStage };
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Button } from "../../../components/Generic/Button";
|
||||
import { Image } from "../../../components/Card/Card";
|
||||
import { classnames } from "../../../utils";
|
||||
import {
|
||||
CLIENT_ID,
|
||||
DEMO_GIST,
|
||||
DEMO_REPO,
|
||||
DEMO_USER,
|
||||
DEMO_WAKATIME_USER,
|
||||
GITHUB_PRIVATE_AUTH_URL,
|
||||
GITHUB_PUBLIC_AUTH_URL,
|
||||
HOST,
|
||||
} from "../../../constants";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
import { logout as _logout } from "../../../redux/actions/userActions";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
usePrivateAccess,
|
||||
useUserId,
|
||||
useUserKey,
|
||||
} from "../../../redux/selectors/userSelectors";
|
||||
import { deleteAccount } from "../../../api";
|
||||
|
||||
const LoginStage = ({ setCurrItem }) => {
|
||||
const userId = useUserId(null);
|
||||
const userKey = useUserKey();
|
||||
const privateAccess = usePrivateAccess();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
|
||||
const logout = () => {
|
||||
dispatch(_logout());
|
||||
};
|
||||
|
||||
const openDeleteModal = () => {
|
||||
setDeleteModal(true);
|
||||
};
|
||||
|
||||
const closeDeleteModal = () => {
|
||||
setDeleteModal(false);
|
||||
};
|
||||
|
||||
function useOutsideAlerter(ref, action) {
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Alert if clicked on outside of element
|
||||
*/
|
||||
function handleClickOutside(event) {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref]);
|
||||
}
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
useOutsideAlerter(wrapperRef, closeDeleteModal);
|
||||
|
||||
const deleteAccountHandler = async () => {
|
||||
const success = await deleteAccount(userId, userKey);
|
||||
if (success) {
|
||||
logout();
|
||||
window.location = `https://github.com/settings/connections/applications/${CLIENT_ID}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Card data
|
||||
const cards = [
|
||||
{
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/top-langs?username=${DEMO_USER}&langs_count=4&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `?username=${DEMO_USER}&include_all_commits=true&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}&disable_animations=true`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-wrap">
|
||||
<div className={classnames(deleteModal ? "opacity-25" : "", "md:flex")}>
|
||||
<div className="lg:block lg:w-3/5 lg:p-8">
|
||||
<div
|
||||
className={classnames(
|
||||
"bg-gray-200 rounded-sm w-full h-full m-auto p-8 shadow",
|
||||
"lg:h-auto",
|
||||
)}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
{/* Access Level Management Buttons */}
|
||||
<div className="mb-4">
|
||||
{privateAccess ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href={`https://${HOST}/api/downgrade?user_key=${userKey}`}
|
||||
>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Downgrade to Public Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Switch to public access if you prefer not to share
|
||||
private contributions.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Upgrade to Private Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Upgrade to include contributions in private repositories
|
||||
for more complete and accurate stats.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Account Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
<span className="xl:text-lg text-red-600">
|
||||
Delete Account
|
||||
</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
This will delete your GitHub-Stats-Extended account and then
|
||||
redirect you to a GitHub screen where you can revoke your
|
||||
access token.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logout Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={logout}
|
||||
>
|
||||
<span className="xl:text-lg">Log Out</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Log out from GitHub-Stats-Extended.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* User is not logged in - show login options */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PUBLIC_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
GitHub Public Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Generate stats based on your contributions in public
|
||||
repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
GitHub Private Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Include contributions from private repositories for more
|
||||
complete and accurate stats.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={() => setCurrItem(1)}
|
||||
>
|
||||
<span className="ml-2 xl:text-lg">Continue as Guest</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Explore options using sample data. Insert your own username
|
||||
in the last step.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
|
||||
<div className="relative w-full h-full">
|
||||
{cards.map((card, index) => {
|
||||
const radius = 60;
|
||||
const centerX = 70;
|
||||
const startAngle = Math.PI * 0.75; // 135 degrees
|
||||
const endAngle = Math.PI * 1.25; // 225 degrees
|
||||
|
||||
const angle =
|
||||
startAngle +
|
||||
(endAngle - startAngle) * (index / (cards.length - 1));
|
||||
const x = centerX + radius * Math.cos(angle);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
left: `${x}%`,
|
||||
position: "relative",
|
||||
zoom: "0.5",
|
||||
marginBottom: "1%",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
imageSrc={card.demoImageSrc}
|
||||
compact={false}
|
||||
extraClasses=""
|
||||
stage={0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{deleteModal && (
|
||||
<div>
|
||||
<div className="fixed left-0 top-0 w-full h-full">
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<div
|
||||
className="w-96 p-4 bg-white rounded-sm border-2 border-gray-200"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<p className="mb-1 text-2xl text-gray-700">Delete Account</p>
|
||||
<hr />
|
||||
<br />
|
||||
<p>
|
||||
Are you sure you want to delete your account from GitHub
|
||||
Trends?
|
||||
</p>
|
||||
<br />
|
||||
<div className="flex flex-wrap">
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
|
||||
onClick={() => setDeleteModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
|
||||
onClick={deleteAccountHandler}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LoginStage.propTypes = {
|
||||
setCurrItem: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { LoginStage };
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { useIsAuthenticated } from "../../../../redux/selectors/userSelectors";
|
||||
|
||||
import { LoginOptions } from "./LoginOptions";
|
||||
import { LoginAccountManagement } from "./LoginAccountManagement";
|
||||
|
||||
interface LoginStageProps {
|
||||
onContinueAsGuest: () => void;
|
||||
}
|
||||
|
||||
export function LoginStage({
|
||||
onContinueAsGuest,
|
||||
}: LoginStageProps): JSX.Element {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <LoginAccountManagement />;
|
||||
}
|
||||
|
||||
return <LoginOptions onContinueAsGuest={onContinueAsGuest} />;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef, type JSX, type RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import { Button } from "../../../../components/Generic/Button";
|
||||
|
||||
function useOutsideAlerter(
|
||||
ref: RefObject<HTMLElement | null>,
|
||||
action: () => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Alert if clicked on outside of element
|
||||
*/
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [action, ref]);
|
||||
}
|
||||
|
||||
interface LoginAccountDeleteModalProps {
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function LoginAccountDeleteModal(
|
||||
props: LoginAccountDeleteModalProps,
|
||||
): JSX.Element {
|
||||
const { onConfirm, onClose } = props;
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
useOutsideAlerter(wrapperRef, onClose);
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed left-0 top-0 w-full h-full">
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<div
|
||||
className="w-96 p-4 bg-white rounded-sm border-2 border-gray-200"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<p className="mb-1 text-2xl text-gray-700">Delete Account</p>
|
||||
<hr />
|
||||
<br />
|
||||
<p>
|
||||
Are you sure you want to delete your account from GitHub Trends?
|
||||
</p>
|
||||
<br />
|
||||
<div className="flex flex-wrap">
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useCallback, useState, type JSX } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
|
||||
import {
|
||||
usePrivateAccess,
|
||||
useUserId,
|
||||
useUserKey,
|
||||
} from "../../../../redux/selectors/userSelectors";
|
||||
import { deleteAccount } from "../../../../api/user";
|
||||
import { Button } from "../../../../components/Generic/Button";
|
||||
import {
|
||||
CLIENT_ID,
|
||||
HOST,
|
||||
GITHUB_PRIVATE_AUTH_URL,
|
||||
} from "../../../../constants";
|
||||
import { LoginAccountDeleteModal } from "./LoginAccountDeleteModal";
|
||||
import { logout } from "../../../../redux/slices/user";
|
||||
|
||||
import { LoginBox } from "./LoginBox";
|
||||
|
||||
export function LoginAccountManagement(): JSX.Element {
|
||||
const userId = useUserId();
|
||||
const userKey = useUserKey();
|
||||
const privateAccess = usePrivateAccess();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const openDeleteModal = () => {
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const closeDeleteModal = () => {
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
dispatch(logout({ userKey: null }));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleAccountDelete = async () => {
|
||||
const success = await deleteAccount(userId as string, userKey as string);
|
||||
if (success) {
|
||||
handleLogout();
|
||||
window.location.href = `https://github.com/settings/connections/applications/${CLIENT_ID}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginBox isOpaque={showDeleteModal}>
|
||||
<div className="mb-4">
|
||||
{privateAccess ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href={`https://${HOST}/api/downgrade?user_key=${userKey as string}`}
|
||||
>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Downgrade to Public Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Switch to public access if you prefer not to share private
|
||||
contributions.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Upgrade to Private Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Upgrade to include contributions in private repositories for more
|
||||
complete and accurate stats.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Account Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
<span className="xl:text-lg text-red-600">Delete Account</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
This will delete your GitHub-Stats-Extended account and then redirect
|
||||
you to a GitHub screen where you can revoke your access token.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logout Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<span className="xl:text-lg">Log Out</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Log out from GitHub-Stats-Extended.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showDeleteModal && (
|
||||
<LoginAccountDeleteModal
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={() => {
|
||||
void handleAccountDelete();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</LoginBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { JSX, ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { LoginBoxDemoCards } from "./LoginBoxDemoCards";
|
||||
|
||||
interface LoginBoxProps {
|
||||
children: ReactNode;
|
||||
|
||||
isOpaque?: boolean;
|
||||
}
|
||||
|
||||
export function LoginBox(props: LoginBoxProps): JSX.Element {
|
||||
const { children, isOpaque = false } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-wrap">
|
||||
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
|
||||
<div className="lg:block lg:w-3/5 lg:p-8">
|
||||
<div className="bg-gray-200 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginBoxDemoCards />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { CardImage } from "../../../../components/Card/CardImage";
|
||||
import {
|
||||
DEMO_REPO,
|
||||
DEMO_USER,
|
||||
DEMO_WAKATIME_USER,
|
||||
DEMO_GIST,
|
||||
} from "../../../../constants";
|
||||
|
||||
const cards: Array<{ demoImageSrc: string }> = [
|
||||
{
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/top-langs?username=${DEMO_USER}&langs_count=4&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `?username=${DEMO_USER}&include_all_commits=true&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450&disable_animations=true`,
|
||||
},
|
||||
{
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}&disable_animations=true`,
|
||||
},
|
||||
];
|
||||
|
||||
function getCardXPosition(cardIndex: number): number {
|
||||
const radius = 60;
|
||||
const centerX = 70;
|
||||
const startAngle = Math.PI * 0.75; // 135 degrees
|
||||
const endAngle = Math.PI * 1.25; // 225 degrees
|
||||
|
||||
const angle =
|
||||
startAngle + (endAngle - startAngle) * (cardIndex / (cards.length - 1));
|
||||
const x = centerX + radius * Math.cos(angle);
|
||||
return x;
|
||||
}
|
||||
|
||||
export function LoginBoxDemoCards(): JSX.Element {
|
||||
return (
|
||||
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
|
||||
<div className="relative w-full h-full">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={card.demoImageSrc}
|
||||
style={{
|
||||
left: `${getCardXPosition(index)}%`,
|
||||
position: "relative",
|
||||
zoom: "0.5",
|
||||
marginBottom: "1%",
|
||||
}}
|
||||
>
|
||||
<CardImage imageSrc={card.demoImageSrc} compact={false} stage={0} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { JSX } from "react";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
|
||||
import { Button } from "../../../../components/Generic/Button";
|
||||
import {
|
||||
GITHUB_PUBLIC_AUTH_URL,
|
||||
GITHUB_PRIVATE_AUTH_URL,
|
||||
} from "../../../../constants";
|
||||
|
||||
import { LoginBox } from "./LoginBox";
|
||||
|
||||
interface LoginOptionsProps {
|
||||
onContinueAsGuest: () => void;
|
||||
}
|
||||
|
||||
export function LoginOptions(props: LoginOptionsProps): JSX.Element {
|
||||
const { onContinueAsGuest } = props;
|
||||
|
||||
return (
|
||||
<LoginBox>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PUBLIC_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">GitHub Public Access</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Generate stats based on your contributions in public repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">GitHub Private Access</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Include contributions from private repositories for more complete and
|
||||
accurate stats.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
onClick={onContinueAsGuest}
|
||||
>
|
||||
<span className="ml-2 xl:text-lg">Continue as Guest</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
Explore options using sample data. Insert your own username in the
|
||||
last step.
|
||||
</p>
|
||||
</div>
|
||||
</LoginBox>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { Card } from "../../../components/Card/Card";
|
||||
import { useUserId } from "../../../redux/selectors/userSelectors";
|
||||
import {
|
||||
DEMO_GIST,
|
||||
DEMO_REPO,
|
||||
DEMO_USER,
|
||||
DEMO_WAKATIME_USER,
|
||||
} from "../../../constants";
|
||||
import { CardTypes } from "../../../utils";
|
||||
|
||||
const SelectCardStage = ({ selectedCard, setSelectedCard, setStage }) => {
|
||||
return (
|
||||
<div className="w-full flex flex-wrap">
|
||||
{[
|
||||
{
|
||||
title: "GitHub Stats Card",
|
||||
description: "your overall GitHub statistics",
|
||||
demoImageSrc: `?username=${useUserId(DEMO_USER)}&include_all_commits=true`,
|
||||
cardType: CardTypes.STATS,
|
||||
},
|
||||
{
|
||||
title: "Top Languages Card",
|
||||
description: "your most frequently used languages",
|
||||
demoImageSrc: `/top-langs?username=${useUserId(DEMO_USER)}&langs_count=4`,
|
||||
cardType: CardTypes.TOP_LANGS,
|
||||
},
|
||||
{
|
||||
title: "GitHub Extra Pin",
|
||||
description:
|
||||
"pin more than 6 repositories in your profile using a GitHub profile readme",
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}`,
|
||||
cardType: CardTypes.PIN,
|
||||
},
|
||||
{
|
||||
title: "GitHub Gist Pin",
|
||||
description:
|
||||
"pin gists in your GitHub profile using a GitHub profile readme",
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}`,
|
||||
cardType: CardTypes.GIST,
|
||||
},
|
||||
{
|
||||
title: "WakaTime Stats Card",
|
||||
description: "your coding activity from WakaTime",
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450`,
|
||||
cardType: CardTypes.WAKATIME,
|
||||
},
|
||||
].map((card, index) => (
|
||||
<button
|
||||
className="p-2 lg:p-4"
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedCard(card.cardType);
|
||||
setStage(2);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={card.title}
|
||||
description={card.description}
|
||||
imageSrc={card.demoImageSrc}
|
||||
selected={selectedCard === card.cardType}
|
||||
fixedSize="true"
|
||||
stage={1}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectCardStage.propTypes = {
|
||||
selectedCard: PropTypes.string,
|
||||
setSelectedCard: PropTypes.func.isRequired,
|
||||
setStage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SelectCardStage.defaultProps = {
|
||||
selectedCard: null,
|
||||
};
|
||||
|
||||
export { SelectCardStage };
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Card } from "../../../components/Card/Card";
|
||||
import { useUserId } from "../../../redux/selectors/userSelectors";
|
||||
import {
|
||||
DEMO_GIST,
|
||||
DEMO_REPO,
|
||||
DEMO_USER,
|
||||
DEMO_WAKATIME_USER,
|
||||
} from "../../../constants";
|
||||
import { CardType } from "../../../models/CardType";
|
||||
import { useMemo, type JSX } from "react";
|
||||
|
||||
interface SelectCardStageProps {
|
||||
selectedCardType: CardType;
|
||||
onCardTypeChange: (cardType: CardType) => void;
|
||||
}
|
||||
|
||||
export function SelectCardStage({
|
||||
selectedCardType,
|
||||
onCardTypeChange,
|
||||
}: SelectCardStageProps): JSX.Element {
|
||||
const userId = useUserId(DEMO_USER);
|
||||
|
||||
const options = useMemo<
|
||||
Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
demoImageSrc: string;
|
||||
cardType: CardType;
|
||||
}>
|
||||
>(
|
||||
() => [
|
||||
{
|
||||
title: "GitHub Stats Card",
|
||||
description: "your overall GitHub statistics",
|
||||
demoImageSrc: `?username=${userId}&include_all_commits=true`,
|
||||
cardType: CardType.STATS,
|
||||
},
|
||||
{
|
||||
title: "Top Languages Card",
|
||||
description: "your most frequently used languages",
|
||||
demoImageSrc: `/top-langs?username=${userId}&langs_count=4`,
|
||||
cardType: CardType.TOP_LANGS,
|
||||
},
|
||||
{
|
||||
title: "GitHub Extra Pin",
|
||||
description:
|
||||
"pin more than 6 repositories in your profile using a GitHub profile readme",
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}`,
|
||||
cardType: CardType.PIN,
|
||||
},
|
||||
{
|
||||
title: "GitHub Gist Pin",
|
||||
description:
|
||||
"pin gists in your GitHub profile using a GitHub profile readme",
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}`,
|
||||
cardType: CardType.GIST,
|
||||
},
|
||||
{
|
||||
title: "WakaTime Stats Card",
|
||||
description: "your coding activity from WakaTime",
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450`,
|
||||
cardType: CardType.WAKATIME,
|
||||
},
|
||||
],
|
||||
[userId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-wrap">
|
||||
{options.map((card) => (
|
||||
<button
|
||||
className="p-2 lg:p-4"
|
||||
key={card.cardType}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onCardTypeChange(card.cardType);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={card.title}
|
||||
description={card.description}
|
||||
imageSrc={card.demoImageSrc}
|
||||
selected={selectedCardType === card.cardType}
|
||||
fixedSize
|
||||
stage={1}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+31
-20
@@ -1,16 +1,37 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { Card } from "../../../components/Card/Card";
|
||||
// @ts-expect-error this will be provided by the npm package
|
||||
import { themes } from "../../../backend/themes/index";
|
||||
|
||||
const ThemeStage = ({ theme, setTheme, setStage, fullSuffix }) => {
|
||||
// to be removed once npm package has been created
|
||||
type ThemeData = Record<
|
||||
string,
|
||||
{
|
||||
title_color: string;
|
||||
icon_color: string;
|
||||
text_color: string;
|
||||
bg_color: string;
|
||||
border_color: string;
|
||||
}
|
||||
>;
|
||||
|
||||
interface ThemeStageProps {
|
||||
fullSuffix: string;
|
||||
theme: string;
|
||||
onThemeChange: (theme: string) => void;
|
||||
}
|
||||
|
||||
export function ThemeStage({
|
||||
theme,
|
||||
fullSuffix,
|
||||
onThemeChange,
|
||||
}: ThemeStageProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap">
|
||||
{Object.keys(themes)
|
||||
{/* Needed until themes is typed correctly and retrieved from npm package */}
|
||||
{Object.keys(themes as ThemeData)
|
||||
.filter(
|
||||
(myTheme) =>
|
||||
![
|
||||
@@ -22,14 +43,13 @@ const ThemeStage = ({ theme, setTheme, setStage, fullSuffix }) => {
|
||||
"holi",
|
||||
].includes(myTheme),
|
||||
)
|
||||
.map((myTheme, index) => (
|
||||
.map((myTheme) => (
|
||||
<button
|
||||
className="p-2 lg:p-4"
|
||||
key={index}
|
||||
key={myTheme}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTheme(myTheme);
|
||||
setStage(4);
|
||||
onThemeChange(myTheme);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
@@ -55,13 +75,4 @@ const ThemeStage = ({ theme, setTheme, setStage, fullSuffix }) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThemeStage.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
setTheme: PropTypes.func.isRequired,
|
||||
setStage: PropTypes.func.isRequired,
|
||||
fullSuffix: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { ThemeStage };
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export const LOGIN = "LOGIN";
|
||||
export const LOGOUT = "LOGOUT";
|
||||
export const SET_USER_ACCESS = "SET_USER_ACCESS";
|
||||
|
||||
export function login(userId, userKey) {
|
||||
return { type: LOGIN, payload: { userId, userKey } };
|
||||
}
|
||||
|
||||
export function logout(userKey = null) {
|
||||
return { type: LOGOUT, payload: { userKey: userKey } };
|
||||
}
|
||||
|
||||
export function setUserAccess(token, privateAccess) {
|
||||
return {
|
||||
type: SET_USER_ACCESS,
|
||||
payload: { token: token, privateAccess: privateAccess },
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const logger = (store) => (next) => (action) => {
|
||||
import type { Middleware } from "redux";
|
||||
|
||||
const loggerMiddleware: Middleware = (_store) => (next) => (action) => {
|
||||
// console.group(action.type);
|
||||
// console.info('dispatching', action);
|
||||
const result = next(action);
|
||||
// console.log('next state', store.getState());
|
||||
console.groupEnd();
|
||||
// console.groupEnd();
|
||||
return result;
|
||||
};
|
||||
|
||||
export default logger;
|
||||
export { loggerMiddleware };
|
||||
@@ -1,5 +0,0 @@
|
||||
import { combineReducers } from "redux";
|
||||
|
||||
import user from "./user";
|
||||
|
||||
export default combineReducers({ user });
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as types from "../actions/userActions";
|
||||
|
||||
const initialState = {
|
||||
userId: JSON.parse(localStorage.getItem("userId")) || null,
|
||||
userKey: JSON.parse(localStorage.getItem("userKey")) || null,
|
||||
token: null,
|
||||
privateAccess: null,
|
||||
};
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case types.LOGIN:
|
||||
localStorage.setItem("userId", JSON.stringify(action.payload.userId));
|
||||
localStorage.setItem("userKey", JSON.stringify(action.payload.userKey));
|
||||
return {
|
||||
...state,
|
||||
userId: action.payload.userId,
|
||||
userKey: action.payload.userKey,
|
||||
};
|
||||
case types.LOGOUT:
|
||||
if (
|
||||
action.payload.userKey !== null &&
|
||||
action.payload.userKey !== JSON.parse(localStorage.getItem("userKey"))
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
localStorage.clear();
|
||||
return {
|
||||
userId: null,
|
||||
userKey: null,
|
||||
token: null,
|
||||
privateAccess: null,
|
||||
};
|
||||
case types.SET_USER_ACCESS:
|
||||
return {
|
||||
...state,
|
||||
token: action.payload.token,
|
||||
privateAccess: action.payload.privateAccess,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const useUserId = (fallbackUsername) => {
|
||||
return useSelector((state) => state.user.userId) || fallbackUsername;
|
||||
};
|
||||
|
||||
export const useIsAuthenticated = () => {
|
||||
return useSelector((state) => {
|
||||
const userId = state.user.userId;
|
||||
return userId && userId.length > 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const usePrivateAccess = () => {
|
||||
return useSelector((state) => state.user.privateAccess);
|
||||
};
|
||||
|
||||
export const useUserKey = () => {
|
||||
return useSelector((state) => state.user.userKey);
|
||||
};
|
||||
|
||||
export const useUserToken = () => {
|
||||
return useSelector((state) => state.user.token);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import type { StoreState } from "../store";
|
||||
|
||||
export const useUserId = <TUserName extends string | undefined>(
|
||||
fallbackUsername?: TUserName,
|
||||
): TUserName => {
|
||||
const storeValue = useSelector((state: StoreState) => state.user.userId);
|
||||
return (storeValue || fallbackUsername || null) as TUserName;
|
||||
};
|
||||
|
||||
export const useIsAuthenticated = (): boolean => {
|
||||
return useSelector((state: StoreState) => {
|
||||
const userId = state.user.userId;
|
||||
return !!(userId && userId.length > 0);
|
||||
});
|
||||
};
|
||||
|
||||
export const usePrivateAccess = (): string | null => {
|
||||
return useSelector((state: StoreState) => state.user.privateAccess);
|
||||
};
|
||||
|
||||
export const useUserKey = (): string | null => {
|
||||
return useSelector((state: StoreState) => state.user.userKey);
|
||||
};
|
||||
|
||||
export const useUserToken = (): string | null => {
|
||||
return useSelector((state: StoreState) => state.user.token);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Used to snooze knip report.
|
||||
* This export is unused but is required to properly perform infer types on the store:
|
||||
* error TS4023: Exported variable 'store' has or is using name 'UserState' from external module "src/redux/slices/user" but cannot be named.
|
||||
*/
|
||||
export interface UserState {
|
||||
userId: string | null;
|
||||
userKey: string | null;
|
||||
token: string | null;
|
||||
privateAccess: string | null;
|
||||
}
|
||||
|
||||
function getFromLocalStorage(key: string): string | null {
|
||||
const storageValue = localStorage.getItem(key);
|
||||
if (!storageValue) {
|
||||
return null;
|
||||
}
|
||||
return (JSON.parse(storageValue) as string) || null;
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
userId: getFromLocalStorage("userId"),
|
||||
userKey: getFromLocalStorage("userKey"),
|
||||
token: null,
|
||||
privateAccess: null,
|
||||
};
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {
|
||||
login: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string; userKey: string }>,
|
||||
) => {
|
||||
const { userId, userKey } = action.payload;
|
||||
localStorage.setItem("userId", JSON.stringify(userId));
|
||||
localStorage.setItem("userKey", JSON.stringify(userKey));
|
||||
state.userId = userId;
|
||||
state.userKey = userKey;
|
||||
},
|
||||
logout: (state, action: PayloadAction<{ userKey: string | null }>) => {
|
||||
const { userKey } = action.payload;
|
||||
if (
|
||||
userKey !== null &&
|
||||
userKey !== JSON.parse(localStorage.getItem("userKey") as string)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.clear();
|
||||
state.userId = null;
|
||||
state.userKey = null;
|
||||
state.token = null;
|
||||
state.privateAccess = null;
|
||||
},
|
||||
setUserAccess: (
|
||||
state,
|
||||
action: PayloadAction<{ token: string; privateAccess: string }>,
|
||||
) => {
|
||||
const { token, privateAccess } = action.payload;
|
||||
state.token = token;
|
||||
state.privateAccess = privateAccess;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { login, logout, setUserAccess } = userSlice.actions;
|
||||
|
||||
export default userSlice.reducer;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { applyMiddleware, createStore, compose } from "redux";
|
||||
|
||||
import loggerMiddleware from "./logger";
|
||||
import rootReducer from "./reducers";
|
||||
import { USE_LOGGER } from "../constants";
|
||||
|
||||
/**
|
||||
* @param {unknown?} initialState store initial state
|
||||
* @returns {import('redux').Store} store
|
||||
*/
|
||||
export default function configureStore(initialState) {
|
||||
let middlewares = [];
|
||||
if (USE_LOGGER) {
|
||||
middlewares = [loggerMiddleware];
|
||||
}
|
||||
const middlewareEnhancer = applyMiddleware(...middlewares);
|
||||
|
||||
const enhancers = [middlewareEnhancer];
|
||||
const composedEnhancers = compose(...enhancers);
|
||||
|
||||
const store = createStore(rootReducer, initialState, composedEnhancers);
|
||||
|
||||
return store;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
import { USE_LOGGER } from "../constants";
|
||||
|
||||
import { loggerMiddleware } from "./logger";
|
||||
import user from "./slices/user";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
user,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
const middleware = getDefaultMiddleware();
|
||||
if (USE_LOGGER) {
|
||||
return middleware.concat(loggerMiddleware);
|
||||
}
|
||||
return middleware;
|
||||
},
|
||||
});
|
||||
|
||||
export { store };
|
||||
|
||||
export type StoreState = ReturnType<(typeof store)["getState"]>;
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* @param {Array<string>} args list of class names
|
||||
* @returns string classname attribute
|
||||
*/
|
||||
export function classnames(...args) {
|
||||
return args.join(" ");
|
||||
}
|
||||
|
||||
export const CardTypes = {
|
||||
STATS: "stats",
|
||||
TOP_LANGS: "top-langs",
|
||||
PIN: "pin",
|
||||
GIST: "gist",
|
||||
WAKATIME: "wakatime",
|
||||
};
|
||||
@@ -2,17 +2,22 @@ import axios from "axios";
|
||||
import { HOST } from "./constants";
|
||||
|
||||
// See https://github.com/stats-organization/github-stats-extended/pull/27#discussion_r2712184285
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const fetchWakatimeStats = async ({ username, api_domain }) => {
|
||||
const fetchWakatimeStats = async ({
|
||||
username,
|
||||
api_domain: _,
|
||||
}: {
|
||||
username: string;
|
||||
api_domain: string;
|
||||
}): Promise<unknown> => {
|
||||
if (!username) {
|
||||
throw new Error("missing parameter: username");
|
||||
}
|
||||
|
||||
const { data } = await axios.get(
|
||||
const res = await axios.get<unknown>(
|
||||
`https://${HOST}/api/wakatime-proxy?username=${username}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export { fetchWakatimeStats };
|
||||
@@ -1,5 +1,7 @@
|
||||
import daisyui from "daisyui";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
screens: {
|
||||
@@ -16,5 +18,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
plugins: [daisyui],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": ["../../tsconfig.base.json"],
|
||||
"include": ["src", "src/**/*.json", "vite.config.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM"],
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./build-ts",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,27 @@ export default defineConfig({
|
||||
}),
|
||||
|
||||
react(),
|
||||
|
||||
// mock pg (postgres) package in the browser to avoid runtime errors
|
||||
{
|
||||
name: "empty-pg-package",
|
||||
resolveId(id) {
|
||||
if (id === "pg") {
|
||||
return id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
load(id) {
|
||||
if (id === "pg") {
|
||||
return "export default {}";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
external: ["pg"],
|
||||
},
|
||||
|
||||
/** @todo use chunks to split bundle? */
|
||||
chunkSizeWarningLimit: 800,
|
||||
},
|
||||
@@ -41,14 +56,14 @@ export default defineConfig({
|
||||
find: "dotenv",
|
||||
replacement: path.resolve(
|
||||
import.meta.dirname,
|
||||
"src/dotenv-browser-stub.js",
|
||||
"src/dotenv-browser-stub.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: "./src/fetchers/wakatime.js",
|
||||
find: "../src/fetchers/wakatime.js",
|
||||
replacement: path.resolve(
|
||||
import.meta.dirname,
|
||||
"src/wakatime-override.js",
|
||||
"src/wakatime-override.ts",
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
+39
-4
@@ -7,6 +7,7 @@ import jsdoc from "eslint-plugin-jsdoc";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
|
||||
|
||||
@@ -25,8 +26,6 @@ export default defineConfig(
|
||||
},
|
||||
plugins: {
|
||||
jsdoc,
|
||||
react,
|
||||
"react-hooks": reactHooks,
|
||||
},
|
||||
rules: {
|
||||
"no-unexpected-multiline": "error",
|
||||
@@ -64,7 +63,6 @@ export default defineConfig(
|
||||
"no-this-before-super": "error",
|
||||
"object-shorthand": ["warn"],
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"no-negated-condition": "warn",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"keyword-spacing": [
|
||||
@@ -80,6 +78,43 @@ export default defineConfig(
|
||||
"jsdoc/require-jsdoc": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.{d.ts,ts,tsx}"],
|
||||
ignores: ["apps/backend/**"],
|
||||
extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylistic],
|
||||
rules: {
|
||||
"@typescript-eslint/array-type": ["error", { default: "generic" }],
|
||||
"@typescript-eslint/restrict-template-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowAny: false,
|
||||
allowBoolean: true, // for query parameters
|
||||
allowNever: false,
|
||||
allowNullish: false,
|
||||
allowNumber: true,
|
||||
allowRegExp: false,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
args: "all",
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
|
||||
// We don't need this we have typescript
|
||||
"jsdoc/require-returns": "off",
|
||||
"jsdoc/require-returns-description": "off",
|
||||
"jsdoc/require-param-description": "off",
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["apps/backend/**/*.{js}"],
|
||||
languageOptions: {
|
||||
@@ -89,7 +124,7 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["apps/frontend/**/*.{js,jsx}"],
|
||||
files: ["apps/frontend/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
react,
|
||||
"react-hooks": reactHooks,
|
||||
|
||||
+3
-2
@@ -20,9 +20,10 @@
|
||||
"apps/frontend": {
|
||||
"entry": [
|
||||
"src/index.jsx",
|
||||
"src/dotenv-browser-stub.js",
|
||||
"src/wakatime-override.js"
|
||||
"src/dotenv-browser-stub.ts", // referenced by vite.config.ts
|
||||
"src/wakatime-override.ts"
|
||||
],
|
||||
|
||||
"ignoreDependencies": [
|
||||
// below dependencies are added because backend folder is copied inside frontend folder,
|
||||
// so some of his dependencies must be present here
|
||||
|
||||
+4
-2
@@ -15,7 +15,8 @@
|
||||
"knip": "5.81.0",
|
||||
"lint-staged": "16.2.7",
|
||||
"prettier": "3.7.4",
|
||||
"typescript": "5.9.3"
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.53.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
@@ -23,7 +24,8 @@
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"lint:knip": "knip"
|
||||
"lint:knip": "knip",
|
||||
"typecheck": "tsc --build --noEmit"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,css,json,jsonc,yaml,yml}": "prettier --write"
|
||||
|
||||
Generated
+291
-87
@@ -44,6 +44,9 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: 8.53.1
|
||||
version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
apps/backend:
|
||||
dependencies:
|
||||
@@ -114,6 +117,9 @@ importers:
|
||||
|
||||
apps/frontend:
|
||||
dependencies:
|
||||
'@reduxjs/toolkit':
|
||||
specifier: 2.11.2
|
||||
version: 2.11.2(react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1
|
||||
version: 1.13.2
|
||||
@@ -129,14 +135,11 @@ importers:
|
||||
github-username-regex:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
prop-types:
|
||||
specifier: ^15.8.1
|
||||
version: 15.8.1
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-icons:
|
||||
specifier: ^4.11.0
|
||||
@@ -145,11 +148,8 @@ importers:
|
||||
specifier: ^3.3.1
|
||||
version: 3.5.0(react@18.3.1)
|
||||
react-redux:
|
||||
specifier: ^8.1.3
|
||||
version: 8.1.3(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.18.0
|
||||
version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1)
|
||||
react-spinners:
|
||||
specifier: ^0.13.8
|
||||
version: 0.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -157,8 +157,8 @@ importers:
|
||||
specifier: ^9.1.3
|
||||
version: 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
redux:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
specifier: 5.0.1
|
||||
version: 5.0.1
|
||||
save-svg-as-png:
|
||||
specifier: ^1.4.17
|
||||
version: 1.4.17
|
||||
@@ -169,12 +169,24 @@ importers:
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: 18.3.27
|
||||
version: 18.3.27
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.7
|
||||
version: 18.3.7(@types/react@18.3.27)
|
||||
'@types/uuid':
|
||||
specifier: 9.0.8
|
||||
version: 9.0.8
|
||||
'@vitejs/plugin-react-swc':
|
||||
specifier: 4.2.2
|
||||
version: 4.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.16
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
clsx:
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
postcss:
|
||||
specifier: ^8.4.31
|
||||
version: 8.5.6
|
||||
@@ -971,9 +983,16 @@ packages:
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@remix-run/router@1.23.2':
|
||||
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
'@reduxjs/toolkit@2.11.2':
|
||||
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.47':
|
||||
resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==}
|
||||
@@ -1147,6 +1166,12 @@ packages:
|
||||
'@sinonjs/fake-timers@13.0.5':
|
||||
resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@swc/core-darwin-arm64@1.15.8':
|
||||
resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1267,11 +1292,6 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7':
|
||||
resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6':
|
||||
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
|
||||
|
||||
@@ -1296,6 +1316,11 @@ packages:
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
'@types/react-dom@18.3.7':
|
||||
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.0.0
|
||||
|
||||
'@types/react@18.3.27':
|
||||
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
|
||||
|
||||
@@ -1305,8 +1330,11 @@ packages:
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
'@types/use-sync-external-store@0.0.3':
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||
'@types/use-sync-external-store@0.0.6':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
'@types/uuid@9.0.8':
|
||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||
|
||||
'@types/yargs-parser@21.0.3':
|
||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
||||
@@ -1314,10 +1342,69 @@ packages:
|
||||
'@types/yargs@17.0.35':
|
||||
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.53.1':
|
||||
resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.53.1
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@8.53.1':
|
||||
resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.53.1':
|
||||
resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.53.1':
|
||||
resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.53.1':
|
||||
resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.53.1':
|
||||
resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@8.52.0':
|
||||
resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/types@8.53.1':
|
||||
resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.53.1':
|
||||
resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@8.53.1':
|
||||
resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.53.1':
|
||||
resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
@@ -1771,6 +1858,10 @@ packages:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@@ -2449,9 +2540,6 @@ packages:
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2504,6 +2592,13 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
ignore@7.0.5:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immer@11.1.3:
|
||||
resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3508,40 +3603,18 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-redux@8.1.3:
|
||||
resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==}
|
||||
react-redux@9.2.0:
|
||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8 || ^17.0 || ^18.0
|
||||
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
react-native: '>=0.59'
|
||||
redux: ^4 || ^5.0.0-beta.0
|
||||
'@types/react': ^18.2.25 || ^19
|
||||
react: ^18.0 || ^19
|
||||
redux: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
|
||||
react-router-dom@6.30.3:
|
||||
resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
react-router@6.30.3:
|
||||
resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
react-spinners@0.13.8:
|
||||
resolution: {integrity: sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==}
|
||||
peerDependencies:
|
||||
@@ -3576,8 +3649,13 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redux@4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||
redux-thunk@3.1.0:
|
||||
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||
peerDependencies:
|
||||
redux: ^5.0.0
|
||||
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
@@ -3591,6 +3669,9 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
reserved-identifiers@1.2.0:
|
||||
resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3978,6 +4059,12 @@ packages:
|
||||
try@1.0.3:
|
||||
resolution: {integrity: sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA==}
|
||||
|
||||
ts-api-utils@2.4.0:
|
||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||
engines: {node: '>=18.12'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
@@ -4037,6 +4124,13 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typescript-eslint@8.53.1:
|
||||
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -5111,7 +5205,17 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@remix-run/router@1.23.2': {}
|
||||
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@standard-schema/utils': 0.3.0
|
||||
immer: 11.1.3
|
||||
redux: 5.0.1
|
||||
redux-thunk: 3.1.0(redux@5.0.1)
|
||||
reselect: 5.1.1
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
react-redux: 9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1)
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.47': {}
|
||||
|
||||
@@ -5218,6 +5322,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 3.0.1
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@swc/core-darwin-arm64@1.15.8':
|
||||
optional: true
|
||||
|
||||
@@ -5332,11 +5440,6 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.27)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.27
|
||||
hoist-non-react-statics: 3.3.2
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
|
||||
'@types/istanbul-lib-report@3.0.3':
|
||||
@@ -5364,6 +5467,10 @@ snapshots:
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@18.3.27)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.27
|
||||
|
||||
'@types/react@18.3.27':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.15
|
||||
@@ -5373,7 +5480,9 @@ snapshots:
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.3': {}
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@types/uuid@9.0.8': {}
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
@@ -5381,8 +5490,99 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.53.1
|
||||
'@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.53.1
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.53.1
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.53.1
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.53.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
debug: 4.4.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.53.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
'@typescript-eslint/visitor-keys': 8.53.1
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.52.0': {}
|
||||
|
||||
'@typescript-eslint/types@8.53.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
'@typescript-eslint/visitor-keys': 8.53.1
|
||||
debug: 4.4.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 8.53.1
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.53.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.1
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
@@ -5886,6 +6086,8 @@ snapshots:
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
collect-v8-coverage@1.0.3: {}
|
||||
@@ -6739,10 +6941,6 @@ snapshots:
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
@@ -6793,6 +6991,10 @@ snapshots:
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immer@11.1.3: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -8009,31 +8211,14 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
react-redux@8.1.3(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1):
|
||||
react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.27)
|
||||
'@types/use-sync-external-store': 0.0.3
|
||||
hoist-non-react-statics: 3.3.2
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
react: 18.3.1
|
||||
react-is: 18.3.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.27
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
redux: 4.2.1
|
||||
|
||||
react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.23.2
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-router: 6.30.3(react@18.3.1)
|
||||
|
||||
react-router@6.30.3(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.23.2
|
||||
react: 18.3.1
|
||||
redux: 5.0.1
|
||||
|
||||
react-spinners@0.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
@@ -8079,9 +8264,11 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redux@4.2.1:
|
||||
redux-thunk@3.1.0(redux@5.0.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
redux: 5.0.1
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
@@ -8105,6 +8292,8 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
reserved-identifiers@1.2.0: {}
|
||||
|
||||
resolve-cwd@3.0.0:
|
||||
@@ -8608,6 +8797,10 @@ snapshots:
|
||||
|
||||
try@1.0.3: {}
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-node@10.9.2(@swc/core@1.15.8)(@types/node@25.0.3)(typescript@5.9.3):
|
||||
@@ -8685,6 +8878,17 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// Type Checking
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Modules
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
|
||||
// Interop Constraints
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./apps/frontend"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user