Improved login, signup and new post creating using js forms
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
parent
6340d248fd
commit
66b617326d
@ -0,0 +1,104 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { FC, FormEvent, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { LoginCredentials } from "@models/login";
|
||||||
|
import { Response } from "@models/response";
|
||||||
|
|
||||||
|
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||||
|
import { parseUserInputError } from "@utils/errors";
|
||||||
|
|
||||||
|
const LoginForm: FC = () => {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const login = useCallback(
|
||||||
|
async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const parseUserInputResult = LoginCredentials.safeParse({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
rememberMe
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parseUserInputResult.success) {
|
||||||
|
setError(parseUserInputError(parseUserInputResult.error.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: Response = await axios
|
||||||
|
.post("/api/login", parseUserInputResult.data)
|
||||||
|
.then(parseAxiosResponse)
|
||||||
|
.catch(parseAxiosError);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
router.push("/blog");
|
||||||
|
} else {
|
||||||
|
setError(JSON.stringify(response.error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[username, password, rememberMe, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={login}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="email">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="form-input"
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
name="username"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="mail@example.com"
|
||||||
|
/>
|
||||||
|
<label className="form-label" htmlFor="password">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
minLength={8}
|
||||||
|
maxLength={128}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
/>
|
||||||
|
<label className="form-checkbox">
|
||||||
|
<input
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={() => setRememberMe((state) => !state)}
|
||||||
|
name="rememberMe"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<i className="form-icon" /> Remember me
|
||||||
|
</label>
|
||||||
|
{error !== null && (
|
||||||
|
<div className="toast toast-error">
|
||||||
|
<button
|
||||||
|
className="btn btn-clear float-right"
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
/>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input className="btn btn-primary" type="submit" value="Login" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginForm;
|
@ -0,0 +1,111 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { FC, FormEvent, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { SignupCredentials } from "@models/signup";
|
||||||
|
import { Response } from "@models/response";
|
||||||
|
|
||||||
|
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||||
|
import { parseUserInputError } from "@utils/errors";
|
||||||
|
|
||||||
|
const SignupForm: FC = () => {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const signup = useCallback(
|
||||||
|
async (e: FormEvent): Promise<void> => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const parseUserInputResult = SignupCredentials.safeParse({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name: username
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parseUserInputResult.success) {
|
||||||
|
setError(parseUserInputError(parseUserInputResult.error.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: Response = await axios
|
||||||
|
.post("/api/signup", parseUserInputResult.data)
|
||||||
|
.then(parseAxiosResponse)
|
||||||
|
.catch(parseAxiosError);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
router.push("/blog");
|
||||||
|
} else {
|
||||||
|
setError(JSON.stringify(response.error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[email, password, username, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={signup}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="email">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="form-input"
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="mail@example.com"
|
||||||
|
/>
|
||||||
|
<label className="form-label" htmlFor="password">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
minLength={8}
|
||||||
|
maxLength={128}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="form-label" htmlFor="password">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Full name"
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
maxLength={32}
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error !== null && (
|
||||||
|
<div className="toast toast-error mt-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-clear float-right"
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
/>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input className="btn btn-primary mt-2" type="submit" value="Signup" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupForm;
|
@ -0,0 +1,27 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
interface Error {
|
||||||
|
ok: false;
|
||||||
|
error?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorModel: z.ZodType<Error> = z.object({
|
||||||
|
ok: z.literal(false),
|
||||||
|
error: z.unknown()
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Ok {
|
||||||
|
ok: true;
|
||||||
|
data?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OkModel: z.ZodType<Ok> = z.object({
|
||||||
|
ok: z.literal(true),
|
||||||
|
data: z.unknown()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Response = Ok | Error;
|
||||||
|
|
||||||
|
const ResponseModel: z.ZodType<Response> = ErrorModel.or(OkModel);
|
||||||
|
|
||||||
|
export default ResponseModel;
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Response } from "@models/response";
|
||||||
|
|
||||||
|
import { NextApiHandler } from "next";
|
||||||
|
|
||||||
|
import { registrationIsEnabled } from "@utils/config";
|
||||||
|
import { methodNotAllowed } from "@utils/errors";
|
||||||
|
|
||||||
|
const handler: NextApiHandler<Response> = (req, res) => {
|
||||||
|
if (req.method?.toUpperCase() !== "GET") {
|
||||||
|
res.status(405).json(methodNotAllowed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ ok: true, data: { registrationIsEnabled } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
@ -1,137 +0,0 @@
|
|||||||
import { GetServerSideProps, NextPage } from "next";
|
|
||||||
import { NextSeo } from "next-seo";
|
|
||||||
|
|
||||||
import Layout from "@components/Layout";
|
|
||||||
|
|
||||||
import multipleClassNames from "@utils/multipleClassNames";
|
|
||||||
|
|
||||||
import styles from "./login.module.scss";
|
|
||||||
import { registrationIsEnabled } from "@utils/config";
|
|
||||||
|
|
||||||
const Login: NextPage<{ registrationEnabled: boolean }> = ({
|
|
||||||
registrationEnabled
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<NextSeo title="Login" />
|
|
||||||
<div className={styles.body}>
|
|
||||||
<div className="columns">
|
|
||||||
<div
|
|
||||||
className={`col-md-4 ${
|
|
||||||
registrationEnabled ? "col-ml-auto" : "col-mx-auto"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<h2 className={styles.title}>Login to blog</h2>
|
|
||||||
<form action="/api/blog/login" method="post">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="email">
|
|
||||||
Email address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
name="username"
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
placeholder="mail@example.com"
|
|
||||||
/>
|
|
||||||
<label className="form-label" htmlFor="password">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
placeholder="Password"
|
|
||||||
minLength={8}
|
|
||||||
maxLength={128}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
/>
|
|
||||||
<label className="form-checkbox">
|
|
||||||
<input name="rememberMe" type="checkbox" />
|
|
||||||
<i className="form-icon" /> Remember me
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className={multipleClassNames(
|
|
||||||
"btn btn-primary",
|
|
||||||
styles.loginButton
|
|
||||||
)}
|
|
||||||
type="submit"
|
|
||||||
value="Login"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{registrationEnabled && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={multipleClassNames("divider-vert", styles.divider)}
|
|
||||||
data-content="OR"
|
|
||||||
/>
|
|
||||||
<div className="col-md-4 col-mr-auto">
|
|
||||||
<h2 className={styles.title}>Create new account</h2>
|
|
||||||
<form action="/api/blog/signup" method="post">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="email">
|
|
||||||
Email address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
placeholder="mail@example.com"
|
|
||||||
/>
|
|
||||||
<label className="form-label" htmlFor="password">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
placeholder="Password"
|
|
||||||
minLength={8}
|
|
||||||
maxLength={128}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="form-label" htmlFor="password">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
placeholder="Full name"
|
|
||||||
maxLength={32}
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className={multipleClassNames(
|
|
||||||
"btn btn-primary",
|
|
||||||
styles.signupButton
|
|
||||||
)}
|
|
||||||
type="submit"
|
|
||||||
value="Signup"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async () => {
|
|
||||||
return { props: { registrationEnabled: registrationIsEnabled } };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
@ -0,0 +1,87 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { NextSeo } from "next-seo";
|
||||||
|
|
||||||
|
import Layout from "@components/Layout";
|
||||||
|
|
||||||
|
import multipleClassNames from "@utils/multipleClassNames";
|
||||||
|
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||||
|
|
||||||
|
import styles from "./login.module.scss";
|
||||||
|
|
||||||
|
import LoginForm from "@components/LoginForm";
|
||||||
|
import SignupForm from "@components/SignupForm";
|
||||||
|
|
||||||
|
const SettingsModel = z.object({ registrationIsEnabled: z.boolean() });
|
||||||
|
|
||||||
|
const Login: NextPage = () => {
|
||||||
|
const { data, isLoading, error } = useQuery<
|
||||||
|
z.infer<typeof SettingsModel>,
|
||||||
|
string
|
||||||
|
>(["settings"], async (): Promise<z.infer<typeof SettingsModel>> => {
|
||||||
|
const response = await axios
|
||||||
|
.get("/api/settings")
|
||||||
|
.then(parseAxiosResponse)
|
||||||
|
.catch(parseAxiosError);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw JSON.stringify(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSettingsResult = SettingsModel.safeParse(response.data);
|
||||||
|
|
||||||
|
if (!parseSettingsResult.success) {
|
||||||
|
throw parseSettingsResult.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseSettingsResult.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationEnabled =
|
||||||
|
data !== undefined && data.registrationIsEnabled && error === null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<NextSeo title="Login" />
|
||||||
|
<div className={styles.body}>
|
||||||
|
<div className="columns">
|
||||||
|
<div
|
||||||
|
className={`col-md-4 ${
|
||||||
|
registrationEnabled ? "col-ml-auto" : "col-mx-auto"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h2 className={styles.title}>Login to blog</h2>
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{registrationEnabled && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={multipleClassNames("divider-vert", styles.divider)}
|
||||||
|
data-content="OR"
|
||||||
|
/>
|
||||||
|
<div className="col-md-4 col-mr-auto">
|
||||||
|
<h2 className={styles.title}>Create new account</h2>
|
||||||
|
<SignupForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="columns">
|
||||||
|
<div className="col col-mx-auto">
|
||||||
|
<div className="loading loading-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
@ -1,8 +1,17 @@
|
|||||||
const baseError = <T>(error: T) => ({ error, ok: false });
|
import z from "zod";
|
||||||
|
|
||||||
export const methodNotAllowed = baseError("Method not allowed");
|
import { Response } from "@models/response";
|
||||||
|
|
||||||
export const unauthorized = {
|
const baseError = (error: string): Response => ({
|
||||||
ok: false,
|
error,
|
||||||
error: "Could not login; incorrect email or password"
|
ok: false
|
||||||
};
|
});
|
||||||
|
|
||||||
|
export const methodNotAllowed: Response = baseError("Method not allowed");
|
||||||
|
|
||||||
|
export const unauthorized: Response = baseError(
|
||||||
|
"Could not login; incorrect email or password"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const parseUserInputError: (error: unknown) => string = (error) =>
|
||||||
|
"Failed to parse user input: ".concat(error);
|
||||||
|
@ -1,53 +1,23 @@
|
|||||||
import axios from "axios";
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import createConfigCatClient from "./createConfigCatClient";
|
import ResponseModel, { ErrorModel, Response } from "@models/response";
|
||||||
|
|
||||||
import {
|
export const parseAxiosResponse = (res: AxiosResponse<unknown>): Response => {
|
||||||
RepositoryResponse,
|
const parseResponseResult = ResponseModel.safeParse(res.data);
|
||||||
SearchResultsResponse,
|
|
||||||
UserResponse
|
|
||||||
} from "@models/responses";
|
|
||||||
import { giteaServerUrl } from "./config";
|
|
||||||
|
|
||||||
const apiUrl = `https://${giteaServerUrl}/api/v1`;
|
if (!parseResponseResult.success) {
|
||||||
|
return { ok: false, error: parseResponseResult.error };
|
||||||
export const fetchAvailability = async (): Promise<boolean> => {
|
}
|
||||||
const configCatClient = createConfigCatClient();
|
|
||||||
|
|
||||||
const isAvailable: boolean =
|
|
||||||
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
|
|
||||||
|
|
||||||
return isAvailable;
|
return parseResponseResult.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUser = async (
|
export const parseAxiosError = (e: AxiosError<unknown>): Response => {
|
||||||
giteaUsername: string
|
const parseErrorResult = ErrorModel.safeParse(e.response?.data);
|
||||||
): Promise<z.infer<typeof UserResponse>> => {
|
|
||||||
const { data: user } = await axios.get<unknown>(
|
|
||||||
`${apiUrl}/users/${giteaUsername}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return UserResponse.parse(user);
|
if (!parseErrorResult.success) {
|
||||||
};
|
return { ok: false, error: parseErrorResult.error };
|
||||||
|
|
||||||
export const fetchRepositories = async (
|
|
||||||
giteaUserUid: number
|
|
||||||
): Promise<z.infer<typeof RepositoryResponse>[]> => {
|
|
||||||
const { data: repositories } = await axios.get<unknown>(
|
|
||||||
`${apiUrl}/repos/search`,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
topic: true,
|
|
||||||
q: "on-portfolio",
|
|
||||||
id: giteaUserUid,
|
|
||||||
limit: 6
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const results = SearchResultsResponse.parse(repositories);
|
|
||||||
|
|
||||||
if (results.ok) return results.data;
|
return parseErrorResult.data;
|
||||||
else throw results.data;
|
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import createConfigCatClient from "@utils/createConfigCatClient";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RepositoryResponse,
|
||||||
|
SearchResultsResponse,
|
||||||
|
UserResponse
|
||||||
|
} from "@models/git/responses";
|
||||||
|
|
||||||
|
import { giteaServerUrl } from "@utils/config";
|
||||||
|
|
||||||
|
const apiUrl = `https://${giteaServerUrl}/api/v1`;
|
||||||
|
|
||||||
|
export const fetchAvailability = async (): Promise<boolean> => {
|
||||||
|
const configCatClient = createConfigCatClient();
|
||||||
|
|
||||||
|
const isAvailable: boolean =
|
||||||
|
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
|
||||||
|
|
||||||
|
return isAvailable;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchUser = async (
|
||||||
|
giteaUsername: string
|
||||||
|
): Promise<z.infer<typeof UserResponse>> => {
|
||||||
|
const { data: user } = await axios.get<unknown>(
|
||||||
|
`${apiUrl}/users/${giteaUsername}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return UserResponse.parse(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchRepositories = async (
|
||||||
|
giteaUserUid: number
|
||||||
|
): Promise<z.infer<typeof RepositoryResponse>[]> => {
|
||||||
|
const { data: repositories } = await axios.get<unknown>(
|
||||||
|
`${apiUrl}/repos/search`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
topic: true,
|
||||||
|
q: "on-portfolio",
|
||||||
|
id: giteaUserUid,
|
||||||
|
limit: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = SearchResultsResponse.parse(repositories);
|
||||||
|
|
||||||
|
if (results.ok) return results.data;
|
||||||
|
else throw results.data;
|
||||||
|
};
|
Loading…
Reference in new issue