mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-23 16:05:24 +00:00
Super commit - repo selection for course submission, course submission required field, course submission link field, badge issuance flow fixed
This commit is contained in:
parent
12693c6637
commit
991a732f4e
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserCourse" ADD COLUMN "submittedRepoLink" TEXT;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Course" ADD COLUMN "submissionRequired" BOOLEAN NOT NULL DEFAULT false;
|
@ -1,14 +1,14 @@
|
|||||||
// datasource db {
|
|
||||||
// provider = "postgresql"
|
|
||||||
// url = env("DATABASE_URL")
|
|
||||||
// }
|
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("POSTGRES_PRISMA_URL")
|
url = env("DATABASE_URL")
|
||||||
directUrl = env("POSTGRES_URL_NON_POOLING")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// datasource db {
|
||||||
|
// provider = "postgresql"
|
||||||
|
// url = env("POSTGRES_PRISMA_URL")
|
||||||
|
// directUrl = env("POSTGRES_URL_NON_POOLING")
|
||||||
|
// }
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
@ -130,6 +130,7 @@ model Course {
|
|||||||
lessons Lesson[]
|
lessons Lesson[]
|
||||||
purchases Purchase[]
|
purchases Purchase[]
|
||||||
noteId String? @unique
|
noteId String? @unique
|
||||||
|
submissionRequired Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
userCourses UserCourse[]
|
userCourses UserCourse[]
|
||||||
@ -218,6 +219,7 @@ model UserCourse {
|
|||||||
completed Boolean @default(false)
|
completed Boolean @default(false)
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
|
submittedRepoLink String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ const UserPurchaseTable = ({ session, windowWidth }) => {
|
|||||||
emptyMessage="No purchases"
|
emptyMessage="No purchases"
|
||||||
value={session.user?.purchased}
|
value={session.user?.purchased}
|
||||||
header={purchasesHeader}
|
header={purchasesHeader}
|
||||||
className="m-2 max-lap:m-0"
|
className="m-2 max-lap:m-0 max-lap:mt-2"
|
||||||
style={{ width: "100%", borderRadius: "8px", border: "1px solid #333", boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.1)" }}
|
style={{ width: "100%", borderRadius: "8px", border: "1px solid #333", boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.1)" }}
|
||||||
pt={{
|
pt={{
|
||||||
wrapper: {
|
wrapper: {
|
||||||
|
66
src/components/profile/RepoSelector.js
Normal file
66
src/components/profile/RepoSelector.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Dropdown } from 'primereact/dropdown';
|
||||||
|
import { useFetchGithubRepos } from '@/hooks/githubQueries/useFetchGithubRepos';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
|
const RepoSelector = ({ courseId, onSubmit }) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const accessToken = session?.account?.access_token;
|
||||||
|
const { data: repos, isLoading } = useFetchGithubRepos(accessToken);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
// Find the existing submission for this course
|
||||||
|
const existingSubmission = useMemo(() => {
|
||||||
|
return session?.user?.userCourses?.find(
|
||||||
|
course => course.courseId === courseId
|
||||||
|
)?.submittedRepoLink;
|
||||||
|
}, [session, courseId]);
|
||||||
|
|
||||||
|
const repoOptions = repos?.map(repo => ({
|
||||||
|
label: repo.name,
|
||||||
|
value: repo.html_url
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const handleRepoSelect = async (repoLink) => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/users/${session.user.id}/courses/${courseId}/submit-repo`, {
|
||||||
|
repoLink
|
||||||
|
});
|
||||||
|
onSubmit(repoLink);
|
||||||
|
showToast('success', 'Success', 'Repository submitted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting repo:', error);
|
||||||
|
showToast('error', 'Error', 'Failed to submit repository');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return (
|
||||||
|
<div className="pl-[28px] mt-2 text-gray-400">
|
||||||
|
GitHub connection required
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pl-[28px] mt-2">
|
||||||
|
<Dropdown
|
||||||
|
value={existingSubmission}
|
||||||
|
options={repoOptions}
|
||||||
|
onChange={(e) => handleRepoSelect(e.value)}
|
||||||
|
placeholder={isLoading ? "Loading repositories..." : "Select a repository"}
|
||||||
|
className="w-full max-w-[300px]"
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
{existingSubmission && (
|
||||||
|
<div className="text-sm text-gray-400 mt-1">
|
||||||
|
✓ Repository submitted
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RepoSelector;
|
@ -61,6 +61,8 @@ const UserBadges = ({ visible, onHide }) => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Current Badge: ", currentBadge);
|
||||||
|
|
||||||
// Only update if this is the first instance or if it's newer than the existing one
|
// Only update if this is the first instance or if it's newer than the existing one
|
||||||
if (!latestBadgeMap.has(defId) ||
|
if (!latestBadgeMap.has(defId) ||
|
||||||
new Date(currentBadge.awardedOn) > new Date(latestBadgeMap.get(defId).awardedOn)) {
|
new Date(currentBadge.awardedOn) > new Date(latestBadgeMap.get(defId).awardedOn)) {
|
||||||
@ -118,7 +120,7 @@ const UserBadges = ({ visible, onHide }) => {
|
|||||||
>
|
>
|
||||||
<div className="relative w-32 h-32 mb-4">
|
<div className="relative w-32 h-32 mb-4">
|
||||||
<Image
|
<Image
|
||||||
src={badge.thumbnail}
|
src={badge.thumbnail || badge.image}
|
||||||
alt={badge.name}
|
alt={badge.name}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
|
@ -7,6 +7,7 @@ import { useBadge } from '@/hooks/badges/useBadge';
|
|||||||
import GenericButton from '@/components/buttons/GenericButton';
|
import GenericButton from '@/components/buttons/GenericButton';
|
||||||
import UserProgressFlow from './UserProgressFlow';
|
import UserProgressFlow from './UserProgressFlow';
|
||||||
import { Tooltip } from 'primereact/tooltip';
|
import { Tooltip } from 'primereact/tooltip';
|
||||||
|
import RepoSelector from '@/components/profile/RepoSelector';
|
||||||
|
|
||||||
const allTasks = [
|
const allTasks = [
|
||||||
{
|
{
|
||||||
@ -32,10 +33,11 @@ const allTasks = [
|
|||||||
status: 'Frontend Course',
|
status: 'Frontend Course',
|
||||||
completed: false,
|
completed: false,
|
||||||
tier: 'Frontend Dev',
|
tier: 'Frontend Dev',
|
||||||
courseId: 'f73c37f4-df2e-4f7d-a838-dce568c76136',
|
// courseId: 'f73c37f4-df2e-4f7d-a838-dce568c76136',
|
||||||
|
courseId: 'af98e096-3136-4d8b-aafe-17677f0266d0',
|
||||||
subTasks: [
|
subTasks: [
|
||||||
{ status: 'Complete the course', completed: false },
|
{ status: 'Complete the course', completed: false },
|
||||||
// { status: 'Select your completed project', completed: false },
|
{ status: 'Submit your project repository', completed: false },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -45,7 +47,7 @@ const allTasks = [
|
|||||||
courseId: 'f6825391-831c-44da-904a-9ac3d149b7be',
|
courseId: 'f6825391-831c-44da-904a-9ac3d149b7be',
|
||||||
subTasks: [
|
subTasks: [
|
||||||
{ status: 'Complete the course', completed: false },
|
{ status: 'Complete the course', completed: false },
|
||||||
// { status: 'Select your completed project', completed: false },
|
{ status: 'Submit your project repository', completed: false },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -87,20 +89,28 @@ const UserProgress = () => {
|
|||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
completed: session?.account?.provider === 'github' ? true : false,
|
completed: session?.account?.provider === 'github' ? true : false,
|
||||||
subTasks: task.subTasks ? task.subTasks.map(subTask => ({
|
subTasks: task.subTasks.map(subTask => ({
|
||||||
...subTask,
|
...subTask,
|
||||||
completed: session?.account?.provider === 'github' ? true : false
|
completed: session?.account?.provider === 'github' ? true : false
|
||||||
})) : undefined
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userCourse = session?.user?.userCourses?.find(uc => uc.courseId === task.courseId);
|
||||||
|
const courseCompleted = completedCourseIds.includes(task.courseId);
|
||||||
|
const repoSubmitted = userCourse?.submittedRepoLink ? true : false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
completed: task.courseId === null || completedCourseIds.includes(task.courseId),
|
completed: courseCompleted && (task.courseId === null || repoSubmitted),
|
||||||
subTasks: task.subTasks ? task.subTasks.map(subTask => ({
|
subTasks: task.subTasks.map(subTask => ({
|
||||||
...subTask,
|
...subTask,
|
||||||
completed: completedCourseIds.includes(task.courseId)
|
completed: subTask.status.includes('Complete')
|
||||||
})) : undefined
|
? courseCompleted
|
||||||
|
: subTask.status.includes('repository')
|
||||||
|
? repoSubmitted
|
||||||
|
: false
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,7 +174,7 @@ const UserProgress = () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
const updatedSession = await getSession();
|
const updatedSession = await getSession();
|
||||||
if (updatedSession?.account?.provider === 'github') {
|
if (updatedSession?.account?.provider === 'github') {
|
||||||
router.push('/'); // Accounts linked successfully
|
router.push('/profile'); // Accounts linked successfully
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -253,19 +263,42 @@ const UserProgress = () => {
|
|||||||
{task.subTasks && (
|
{task.subTasks && (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{task.subTasks.map((subTask, subIndex) => (
|
{task.subTasks.map((subTask, subIndex) => (
|
||||||
<li key={subIndex} className="flex items-center pl-[28px]">
|
<li key={subIndex}>
|
||||||
{subTask.completed ? (
|
<div className="flex items-center pl-[28px]">
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center mr-3">
|
{subTask.completed ? (
|
||||||
<i className="pi pi-check text-white text-sm"></i>
|
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center mr-3">
|
||||||
</div>
|
<i className="pi pi-check text-white text-sm"></i>
|
||||||
) : (
|
</div>
|
||||||
<div className="w-4 h-4 bg-gray-700 rounded-full flex items-center justify-center mr-3">
|
) : (
|
||||||
<i className="pi pi-info-circle text-white text-sm"></i>
|
<div className="w-4 h-4 bg-gray-700 rounded-full flex items-center justify-center mr-3">
|
||||||
</div>
|
<i className="pi pi-info-circle text-white text-sm"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className={`${subTask.completed ? 'text-white' : 'text-gray-400'}`}>
|
||||||
|
{subTask.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{subTask.status.includes('repository') && !subTask.completed && (
|
||||||
|
<RepoSelector
|
||||||
|
courseId={task.courseId}
|
||||||
|
onSubmit={() => {
|
||||||
|
const updatedTasks = tasks.map(t =>
|
||||||
|
t.courseId === task.courseId
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
subTasks: t.subTasks.map(st =>
|
||||||
|
st.status === subTask.status
|
||||||
|
? { ...st, completed: true }
|
||||||
|
: st
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
);
|
||||||
|
setTasks(updatedTasks);
|
||||||
|
router.push('/profile');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<span className={`${subTask.completed ? 'text-white' : 'text-gray-400'}`}>
|
|
||||||
{subTask.status}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -44,6 +44,7 @@ export const createCourse = async (data) => {
|
|||||||
id: courseData.id,
|
id: courseData.id,
|
||||||
noteId: courseData.noteId,
|
noteId: courseData.noteId,
|
||||||
price: courseData.price,
|
price: courseData.price,
|
||||||
|
submissionRequired: courseData.submissionRequired || false,
|
||||||
user: { connect: { id: courseData.user.connect.id } },
|
user: { connect: { id: courseData.user.connect.id } },
|
||||||
lessons: {
|
lessons: {
|
||||||
connect: courseData.lessons.connect
|
connect: courseData.lessons.connect
|
||||||
@ -71,6 +72,7 @@ export const updateCourse = async (id, data) => {
|
|||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...otherData,
|
...otherData,
|
||||||
|
submissionRequired: otherData.submissionRequired || false,
|
||||||
lessons: {
|
lessons: {
|
||||||
deleteMany: {},
|
deleteMany: {},
|
||||||
create: lessons.map((lesson, index) => ({
|
create: lessons.map((lesson, index) => ({
|
||||||
|
@ -65,6 +65,20 @@ export const deleteUserCourse = async (userId, courseId) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const submitCourseRepo = async (userId, courseSlug, repoLink) => {
|
||||||
|
return await prisma.userCourse.update({
|
||||||
|
where: {
|
||||||
|
userId_courseId: {
|
||||||
|
userId,
|
||||||
|
courseId: courseSlug
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
submittedRepoLink: repoLink
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const checkCourseCompletion = async (userId, courseId) => {
|
export const checkCourseCompletion = async (userId, courseId) => {
|
||||||
const course = await prisma.course.findUnique({
|
const course = await prisma.course.findUnique({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
|
@ -25,13 +25,13 @@ export const useBadge = () => {
|
|||||||
// Check for GitHub connection badge
|
// Check for GitHub connection badge
|
||||||
if (session?.account?.provider === 'github') {
|
if (session?.account?.provider === 'github') {
|
||||||
const hasPlebBadge = userBadges?.some(
|
const hasPlebBadge = userBadges?.some(
|
||||||
userBadge => userBadge.badge?.id === '3664e78f-b618-420d-a7cc-f3393b0211df'
|
userBadge => userBadge.badge?.id === '4664e73f-c618-41dd-a7cc-f3393b031fdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasPlebBadge) {
|
if (!hasPlebBadge) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/badges/issue', {
|
const response = await axios.post('/api/badges/issue', {
|
||||||
badgeId: '3664e78f-b618-420d-a7cc-f3393b0211df',
|
badgeId: '4664e73f-c618-41dd-a7cc-f3393b031fdf',
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,8 +51,12 @@ export const useBadge = () => {
|
|||||||
userBadge => userBadge.badge?.courseId === userCourse.courseId
|
userBadge => userBadge.badge?.courseId === userCourse.courseId
|
||||||
);
|
);
|
||||||
const hasBadgeDefined = !!userCourse.course?.badge;
|
const hasBadgeDefined = !!userCourse.course?.badge;
|
||||||
|
|
||||||
|
// Check if course requires repo submission
|
||||||
|
const requiresRepo = userCourse.course?.submissionRequired ?? false;
|
||||||
|
const hasRepoIfRequired = requiresRepo ? !!userCourse.submittedRepoLink : true;
|
||||||
|
|
||||||
return isCompleted && hasNoBadge && hasBadgeDefined;
|
return isCompleted && hasNoBadge && hasBadgeDefined && hasRepoIfRequired;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const course of eligibleCourses || []) {
|
for (const course of eligibleCourses || []) {
|
||||||
|
@ -8,26 +8,46 @@ export function useFetchGithubRepos(accessToken) {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['githubRepos', accessToken],
|
queryKey: ['githubRepos', accessToken],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!accessToken) return [];
|
if (!accessToken) {
|
||||||
|
console.log('No access token provided');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const octokit = new ThrottledOctokit({
|
try {
|
||||||
auth: accessToken,
|
const octokit = new ThrottledOctokit({
|
||||||
throttle: {
|
auth: accessToken,
|
||||||
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
throttle: {
|
||||||
if (retryCount < 2) return true;
|
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
||||||
|
console.log(`Rate limit exceeded, retrying after ${retryAfter} seconds`);
|
||||||
|
if (retryCount < 2) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onSecondaryRateLimit: (retryAfter, options, octokit) => {
|
||||||
|
console.log(`Secondary rate limit hit, retrying after ${retryAfter} seconds`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onSecondaryRateLimit: (retryAfter, options, octokit) => true,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = await octokit.repos.listForAuthenticatedUser({
|
console.log('Fetching repositories...');
|
||||||
sort: 'updated',
|
const { data } = await octokit.repos.listForAuthenticatedUser({
|
||||||
per_page: 100
|
sort: 'updated',
|
||||||
});
|
per_page: 100
|
||||||
|
});
|
||||||
|
console.log(`Found ${data.length} repositories`);
|
||||||
|
|
||||||
return data;
|
return data.map(repo => ({
|
||||||
|
id: repo.id,
|
||||||
|
name: repo.name,
|
||||||
|
html_url: repo.html_url
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching GitHub repos:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
enabled: !!accessToken
|
enabled: !!accessToken,
|
||||||
|
retry: 3,
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -51,6 +51,14 @@ export default async function handler(req, res) {
|
|||||||
return res.status(400).json({ error: 'Course not completed' });
|
return res.status(400).json({ error: 'Course not completed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if course requires repo submission
|
||||||
|
if (userCourse.course.submissionRequired && !userCourse.submittedRepoLink) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Repository submission required',
|
||||||
|
message: 'You must submit a project repository to earn this badge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
badge = userCourse.course.badge;
|
badge = userCourse.course.badge;
|
||||||
} else if (badgeId) {
|
} else if (badgeId) {
|
||||||
// Direct badge lookup for non-course badges
|
// Direct badge lookup for non-course badges
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "@/pages/api/auth/[...nextauth].js";
|
||||||
|
import { submitCourseRepo } from "@/db/models/userCourseModels";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { courseSlug } = req.query;
|
||||||
|
const { repoLink } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submitCourseRepo(session.user.id, courseSlug, repoLink);
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error: 'Failed to submit repo' });
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user