diff --git a/backend/bin/fetchAvoinLinks.ts b/backend/bin/fetchAvoinLinks.ts index e82c8a08f..1f022f001 100644 --- a/backend/bin/fetchAvoinLinks.ts +++ b/backend/bin/fetchAvoinLinks.ts @@ -100,7 +100,7 @@ const getInfoWithCourseCode = async ( const res = await axios.get(url, { headers: { Authorized: "Basic " + AVOIN_TOKEN }, }) - return await res.data + return res.data } interface AvoinLinkData { diff --git a/backend/bin/importOrganizations.ts b/backend/bin/importOrganizations.ts index 178621cb2..4e291ac17 100644 --- a/backend/bin/importOrganizations.ts +++ b/backend/bin/importOrganizations.ts @@ -118,7 +118,7 @@ const getUserFromTmc = async (user_id: number) => { username: details.username, } - return await prisma.user.upsert({ + return prisma.user.upsert({ where: { upstream_id: details.id }, create: prismaDetails, update: convertUpdate(prismaDetails), diff --git a/backend/bin/kafkaConsumer/common/userPoints/__test__/saveToDB.test.ts b/backend/bin/kafkaConsumer/common/userPoints/__test__/saveToDB.test.ts index f457a5939..c95666578 100644 --- a/backend/bin/kafkaConsumer/common/userPoints/__test__/saveToDB.test.ts +++ b/backend/bin/kafkaConsumer/common/userPoints/__test__/saveToDB.test.ts @@ -24,6 +24,10 @@ const tmc = fakeTMCSpecific({ 9999: [404, { errors: "asdf" }], }) +function expectIsDefined(value: T | null | undefined): asserts value is T { + expect(value).toBeDefined() +} + describe("userPoints/saveToDatabase", () => { const kafkaContext = {} as KafkaContext @@ -193,6 +197,12 @@ describe("userPoints/saveToDatabase", () => { }, }) + expectIsDefined(existing) + expect( + existing.exercise_completion_required_actions.map((ra) => ra.value) + .length, + ).toEqual(0) + const ret = await saveToDatabase(kafkaContext, { ...message, timestamp: "2000-03-01T10:00:00.00+02:00", @@ -211,8 +221,10 @@ describe("userPoints/saveToDatabase", () => { exercise_completion_required_actions: true, }, }) - - expect(updated?.updated_at! > existing?.updated_at!).toBeTruthy() + expectIsDefined(updated) + expect(updated.updated_at?.valueOf() ?? -Infinity).toBeGreaterThan( + existing.updated_at?.valueOf() ?? -Infinity, + ) expect( updated?.exercise_completion_required_actions .map((ra) => ra.value) diff --git a/backend/bin/lib/await-semaphore.ts b/backend/bin/lib/await-semaphore.ts index 571cd06ba..ea96616a3 100644 --- a/backend/bin/lib/await-semaphore.ts +++ b/backend/bin/lib/await-semaphore.ts @@ -9,9 +9,9 @@ export class Semaphore { private sched() { if (this.count > 0 && this.tasks.length > 0) { this.count-- - let next = this.tasks.shift() + const next = this.tasks.shift() if (next === undefined) { - throw "Unexpected undefined value in tasks list" + throw new Error("Unexpected undefined value in tasks list") } next() @@ -20,8 +20,8 @@ export class Semaphore { public acquire() { return new Promise<() => void>((res, _) => { - var task = () => { - var released = false + const task = () => { + let released = false res(() => { if (!released) { released = true diff --git a/backend/bin/seed.ts b/backend/bin/seed.ts index 8f59c6c95..cfeeef162 100644 --- a/backend/bin/seed.ts +++ b/backend/bin/seed.ts @@ -392,13 +392,13 @@ const seed = async () => { : undefined, } - return await prisma.studyModule.create({ + return prisma.studyModule.create({ data: _module, }) }), ) - return await Promise.all( + return Promise.all( Courses.map(async (course) => { const _course = { ...course, diff --git a/backend/bin/seedPoints.ts b/backend/bin/seedPoints.ts index c3d13ffdb..9e1ea2794 100644 --- a/backend/bin/seedPoints.ts +++ b/backend/bin/seedPoints.ts @@ -75,7 +75,7 @@ const addServices = async () => { const addUserCourseProgressess = async (courseId: string) => { const usersInDb = await prisma.user.findMany({ take: 100 }) - return await Promise.all( + return Promise.all( usersInDb.map(async (user) => { const progress = [ { @@ -126,14 +126,14 @@ const addUserCourseProgressess = async (courseId: string) => { max_points: progress.reduce((acc, curr) => acc + curr.max_points, 0), } - return await prisma.userCourseProgress.create({ data: ucp }) + return prisma.userCourseProgress.create({ data: ucp }) }), ) } const addUserCourseSettingses = async (courseId: string) => { const UsersInDb = await prisma.user.findMany({ take: 100 }) - return await Promise.all( + return Promise.all( UsersInDb.map(async (user) => { const ucs: Prisma.UserCourseSettingCreateInput = { user: { @@ -153,7 +153,7 @@ const addUserCourseSettingses = async (courseId: string) => { course_variant: null, other: null, } - return await prisma.userCourseSetting.create({ data: ucs }) + return prisma.userCourseSetting.create({ data: ucs }) }), ) } diff --git a/backend/graphql/ABEnrollment.ts b/backend/graphql/ABEnrollment.ts index a4bb84473..9c5e67ca0 100644 --- a/backend/graphql/ABEnrollment.ts +++ b/backend/graphql/ABEnrollment.ts @@ -52,30 +52,31 @@ export const ABEnrollmentMutations = extendType({ }, }) }, - }), - t.field("updateAbEnrollment", { - type: "AbEnrollment", - args: { - abEnrollment: nonNull( - arg({ - type: "AbEnrollmentCreateOrUpsertInput", - }), - ), - }, - authorize: isAdmin, - resolve: async (_, { abEnrollment }, ctx: Context) => { - const { user_id, ab_study_id } = abEnrollment + }) - return ctx.prisma.abEnrollment.update({ - where: { - user_id_ab_study_id: { - user_id, - ab_study_id, - }, + t.field("updateAbEnrollment", { + type: "AbEnrollment", + args: { + abEnrollment: nonNull( + arg({ + type: "AbEnrollmentCreateOrUpsertInput", + }), + ), + }, + authorize: isAdmin, + resolve: async (_, { abEnrollment }, ctx: Context) => { + const { user_id, ab_study_id } = abEnrollment + + return ctx.prisma.abEnrollment.update({ + where: { + user_id_ab_study_id: { + user_id, + ab_study_id, }, - data: abEnrollment, - }) - }, - }) + }, + data: abEnrollment, + }) + }, + }) }, }) diff --git a/backend/graphql/ABStudy.ts b/backend/graphql/ABStudy.ts index 34ce6f0f2..584251f34 100644 --- a/backend/graphql/ABStudy.ts +++ b/backend/graphql/ABStudy.ts @@ -52,25 +52,26 @@ export const ABStudyMutations = extendType({ data: abStudy, }) }, - }), - t.field("updateAbStudy", { - type: "AbStudy", - args: { - abStudy: nonNull( - arg({ - type: "AbStudyUpsertInput", - }), - ), - }, - authorize: isAdmin, - resolve: async (_, { abStudy }, ctx: Context) => { - const { id } = abStudy + }) - return ctx.prisma.abStudy.update({ - where: { id }, - data: abStudy, - }) - }, - }) + t.field("updateAbStudy", { + type: "AbStudy", + args: { + abStudy: nonNull( + arg({ + type: "AbStudyUpsertInput", + }), + ), + }, + authorize: isAdmin, + resolve: async (_, { abStudy }, ctx: Context) => { + const { id } = abStudy + + return ctx.prisma.abStudy.update({ + where: { id }, + data: abStudy, + }) + }, + }) }, }) diff --git a/backend/graphql/Completion/model.ts b/backend/graphql/Completion/model.ts index c08506111..cb217e063 100644 --- a/backend/graphql/Completion/model.ts +++ b/backend/graphql/Completion/model.ts @@ -104,7 +104,7 @@ export const Completion = objectType({ }, }) - t.field("registered", { + t.nonNull.field("registered", { type: "Boolean", resolve: async (parent, _, ctx) => { const registered = await ctx.prisma.completion @@ -117,7 +117,7 @@ export const Completion = objectType({ }, }) - t.field("project_completion", { + t.nonNull.field("project_completion", { type: "Boolean", resolve: async (parent, _, ctx) => { if (!parent.course_id) { @@ -149,7 +149,7 @@ export const Completion = objectType({ }, }) - t.field("certificate_availability", { + t.nullable.field("certificate_availability", { type: "CertificateAvailability", resolve: async ({ course_id, user_upstream_id }, _, ctx) => { if (!course_id) { diff --git a/backend/graphql/Completion/mutations.ts b/backend/graphql/Completion/mutations.ts index 9d1c679f4..62df860cb 100644 --- a/backend/graphql/Completion/mutations.ts +++ b/backend/graphql/Completion/mutations.ts @@ -59,7 +59,7 @@ export const CompletionMutations = extendType({ }, }) - t.list.field("addManualCompletion", { + t.list.nonNull.field("addManualCompletion", { type: "Completion", args: { completions: list(arg({ type: "ManualCompletionArg" })), diff --git a/backend/graphql/Completion/queries.ts b/backend/graphql/Completion/queries.ts index d9dd11791..7a9684a0d 100644 --- a/backend/graphql/Completion/queries.ts +++ b/backend/graphql/Completion/queries.ts @@ -14,7 +14,7 @@ import { buildUserSearch } from "../common" export const CompletionQueries = extendType({ type: "Query", definition(t) { - t.list.field("completions", { + t.list.nonNull.field("completions", { type: "Completion", args: { course: nonNull(stringArg()), diff --git a/backend/graphql/CompletionRegistered.ts b/backend/graphql/CompletionRegistered.ts index 001dfcc42..97624ab9b 100644 --- a/backend/graphql/CompletionRegistered.ts +++ b/backend/graphql/CompletionRegistered.ts @@ -39,7 +39,7 @@ export const CompletionRegistered = objectType({ export const CompletionRegisteredQueries = extendType({ type: "Query", definition(t) { - t.list.field("registeredCompletions", { + t.list.nonNull.field("registeredCompletions", { type: "CompletionRegistered", args: { course: stringArg({ description: "course slug or course alias" }), @@ -93,7 +93,7 @@ export const CompletionRegisteredQueries = extendType({ export const CompletionRegisteredMutations = extendType({ type: "Mutation", definition(t) { - t.field("registerCompletion", { + t.nonNull.field("registerCompletion", { type: "String", args: { completions: nonNull(list(nonNull(arg({ type: "CompletionArg" })))), @@ -102,8 +102,8 @@ export const CompletionRegisteredMutations = extendType({ resolve: async (_, args, ctx: Context) => { const queue = chunk(args.completions, 500) - for (let i = 0; i < queue.length; i++) { - const promises = buildPromises(queue[i], ctx) + for (const element of queue) { + const promises = buildPromises(element, ctx) await Promise.all(promises) } return "success" diff --git a/backend/graphql/Course/input.ts b/backend/graphql/Course/input.ts index 4dcf6287b..47f6af342 100644 --- a/backend/graphql/Course/input.ts +++ b/backend/graphql/Course/input.ts @@ -19,19 +19,19 @@ export const CourseCreateArg = inputObjectType({ t.string("support_email") t.nonNull.datetime("start_date") t.datetime("end_date") - t.list.field("study_modules", { + t.list.nonNull.field("study_modules", { type: "StudyModuleWhereUniqueInput", }) - t.list.nullable.field("course_translations", { + t.list.nonNull.field("course_translations", { type: "CourseTranslationCreateInput", }) - t.list.nullable.field("open_university_registration_links", { + t.list.nonNull.field("open_university_registration_links", { type: "OpenUniversityRegistrationLinkCreateInput", }) - t.list.nullable.field("course_variants", { + t.list.nonNull.field("course_variants", { type: "CourseVariantCreateInput", }) - t.list.nullable.field("course_aliases", { + t.list.nonNull.field("course_aliases", { type: "CourseAliasCreateInput", }) t.int("order") @@ -43,7 +43,7 @@ export const CourseCreateArg = inputObjectType({ t.nullable.id("inherit_settings_from") t.nullable.id("completions_handled_by") t.nullable.boolean("has_certificate") - t.list.nullable.field("user_course_settings_visibilities", { + t.list.nonNull.field("user_course_settings_visibilities", { type: "UserCourseSettingsVisibilityCreateInput", }) t.nullable.boolean("upcoming_active_link") @@ -76,19 +76,19 @@ export const CourseUpsertArg = inputObjectType({ t.string("support_email") t.nonNull.datetime("start_date") t.datetime("end_date") - t.list.field("study_modules", { + t.list.nonNull.field("study_modules", { type: "StudyModuleWhereUniqueInput", }) - t.list.nullable.field("course_translations", { + t.list.nonNull.field("course_translations", { type: "CourseTranslationUpsertInput", }) - t.list.nullable.field("open_university_registration_links", { + t.list.nonNull.field("open_university_registration_links", { type: "OpenUniversityRegistrationLinkUpsertInput", }) - t.list.nullable.field("course_variants", { + t.list.nonNull.field("course_variants", { type: "CourseVariantUpsertInput", }) - t.list.nullable.field("course_aliases", { + t.list.nonNull.field("course_aliases", { type: "CourseAliasUpsertInput", }) t.int("order") @@ -100,7 +100,7 @@ export const CourseUpsertArg = inputObjectType({ t.nullable.id("inherit_settings_from") t.nullable.id("completions_handled_by") t.nullable.boolean("has_certificate") - t.list.nullable.field("user_course_settings_visibilities", { + t.list.nonNull.field("user_course_settings_visibilities", { type: "UserCourseSettingsVisibilityUpsertInput", }) t.nullable.boolean("upcoming_active_link") diff --git a/backend/graphql/Course/model.ts b/backend/graphql/Course/model.ts index fd51f9ffb..388db21ba 100644 --- a/backend/graphql/Course/model.ts +++ b/backend/graphql/Course/model.ts @@ -64,7 +64,7 @@ export const Course = objectType({ t.string("instructions") t.string("link") - t.list.field("completions", { + t.list.nonNull.field("completions", { type: "Completion", args: { user_id: nullable(stringArg()), @@ -103,7 +103,7 @@ export const Course = objectType({ }, }) - t.list.field("exercises", { + t.list.nonNull.field("exercises", { type: "Exercise", args: { includeDeleted: booleanArg({ default: false }), diff --git a/backend/graphql/Course/queries.ts b/backend/graphql/Course/queries.ts index 186fb48c8..1626817b4 100644 --- a/backend/graphql/Course/queries.ts +++ b/backend/graphql/Course/queries.ts @@ -97,7 +97,7 @@ export const CourseQueries = extendType({ ordering: true, }) - t.list.field("courses", { + t.list.nonNull.field("courses", { type: "Course", args: { orderBy: arg({ type: "CourseOrderByInput" }), @@ -210,7 +210,7 @@ export const CourseQueries = extendType({ }, }) - t.list.field("handlerCourses", { + t.list.nonNull.field("handlerCourses", { type: "Course", authorize: isAdmin, resolve: async (_, __, ctx) => { @@ -224,7 +224,7 @@ export const CourseQueries = extendType({ }, }) - t.field("course_exists", { + t.nonNull.field("course_exists", { type: "Boolean", args: { slug: nonNull(stringArg()), diff --git a/backend/graphql/CourseOrganization.ts b/backend/graphql/CourseOrganization.ts index 752f56dee..fe4069f08 100644 --- a/backend/graphql/CourseOrganization.ts +++ b/backend/graphql/CourseOrganization.ts @@ -21,7 +21,7 @@ export const CourseOrganization = objectType({ export const CourseOrganizationQueries = extendType({ type: "Query", definition(t) { - t.list.field("courseOrganizations", { + t.list.nonNull.field("courseOrganizations", { type: "CourseOrganization", args: { course_id: idArg(), diff --git a/backend/graphql/CourseStatsSubscription.ts b/backend/graphql/CourseStatsSubscription.ts index 3cc13b129..638d60693 100644 --- a/backend/graphql/CourseStatsSubscription.ts +++ b/backend/graphql/CourseStatsSubscription.ts @@ -27,7 +27,6 @@ export const CourseStatsSubscriptionMutations = extendType({ authorize: or(isUser, isAdmin), resolve: async (_, { id }, ctx) => { const { user } = ctx - return ctx.prisma.courseStatsSubscription.create({ data: { user: { connect: { id: user?.id } }, @@ -61,7 +60,7 @@ export const CourseStatsSubscriptionMutations = extendType({ } } - return await ctx.prisma.courseStatsSubscription.delete({ + return ctx.prisma.courseStatsSubscription.delete({ where: { id, }, diff --git a/backend/graphql/CourseTranslation.ts b/backend/graphql/CourseTranslation.ts index 12995fb4e..6681b4f15 100644 --- a/backend/graphql/CourseTranslation.ts +++ b/backend/graphql/CourseTranslation.ts @@ -54,7 +54,7 @@ export const CourseTranslationUpsertInput = inputObjectType({ export const CourseTranslationQueries = extendType({ type: "Query", definition(t) { - t.list.field("courseTranslations", { + t.list.nonNull.field("courseTranslations", { type: "CourseTranslation", args: { language: stringArg(), diff --git a/backend/graphql/CourseVariant.ts b/backend/graphql/CourseVariant.ts index 31b013611..ed2eb5651 100644 --- a/backend/graphql/CourseVariant.ts +++ b/backend/graphql/CourseVariant.ts @@ -55,7 +55,7 @@ export const CourseVariantQueries = extendType({ ctx.prisma.courseVariant.findUnique({ where: { id } }), }) - t.list.field("courseVariants", { + t.list.nonNull.field("courseVariants", { type: "CourseVariant", args: { course_id: idArg(), diff --git a/backend/graphql/EmailTemplate.ts b/backend/graphql/EmailTemplate.ts index 60c61517a..8bd0ab8f8 100644 --- a/backend/graphql/EmailTemplate.ts +++ b/backend/graphql/EmailTemplate.ts @@ -48,7 +48,7 @@ export const EmailTemplateQueries = extendType({ }), }) - t.list.field("email_templates", { + t.list.nonNull.field("email_templates", { type: "EmailTemplate", authorize: isAdmin, resolve: (_, __, ctx) => ctx.prisma.emailTemplate.findMany(), diff --git a/backend/graphql/Exercise.ts b/backend/graphql/Exercise.ts index 2ec40437e..7e8a46400 100644 --- a/backend/graphql/Exercise.ts +++ b/backend/graphql/Exercise.ts @@ -32,7 +32,7 @@ export const Exercise = objectType({ t.model.timestamp() t.model.updated_at() - t.list.field("exercise_completions", { + t.list.nonNull.field("exercise_completions", { type: "ExerciseCompletion", args: { orderBy: nullable( @@ -80,7 +80,7 @@ export const ExerciseQueries = extendType({ }, authorize: isAdmin, resolve: async (_, { id }, ctx) => - await ctx.prisma.exercise.findUnique({ + ctx.prisma.exercise.findUnique({ where: { id }, }), }) diff --git a/backend/graphql/Organization.ts b/backend/graphql/Organization.ts index 79ea4fd83..ed935a753 100644 --- a/backend/graphql/Organization.ts +++ b/backend/graphql/Organization.ts @@ -13,6 +13,8 @@ import { stringArg, } from "nexus" +import { Prisma } from "@prisma/client" + import { isAdmin, Role } from "../accessControl" import { Context } from "../context" import { filterNullFields } from "../util" @@ -125,7 +127,7 @@ export const OrganizationQueries = extendType({ authorize: organizationQueryHiddenOrDisabledPermission, }) - t.list.field("organizations", { + t.list.nonNull.field("organizations", { type: "Organization", args: { take: intArg(), @@ -143,6 +145,24 @@ export const OrganizationQueries = extendType({ { take, skip, cursor, orderBy, hidden, disabled }, ctx, ) => { + const where: Prisma.OrganizationWhereInput = {} + + if (!hidden || !disabled) { + where.AND = [] + + if (!hidden) { + where.AND.push({ + OR: [{ hidden: false }, { hidden: null }], + }) + } + + if (!disabled) { + where.AND.push({ + OR: [{ disabled: false }, { disabled: null }], + }) + } + } + return ctx.prisma.organization.findMany({ ...filterNullFields({ take, @@ -154,19 +174,7 @@ export const OrganizationQueries = extendType({ }, }), orderBy: filterNullFields(orderBy), - where: { - ...(!hidden && { hidden: { not: true } }), - ...(!disabled && { - OR: [ - { - disabled: false, - }, - { - disabled: null, - }, - ], - }), - }, + where, }) }, }) diff --git a/backend/graphql/Progress.ts b/backend/graphql/Progress.ts index 66801eea2..b9944a309 100644 --- a/backend/graphql/Progress.ts +++ b/backend/graphql/Progress.ts @@ -29,7 +29,7 @@ export const Progress = objectType({ }, }) - t.list.field("user_course_service_progresses", { + t.list.nonNull.field("user_course_service_progresses", { type: "UserCourseServiceProgress", resolve: async (parent, _, ctx) => { const course_id = parent.course?.id diff --git a/backend/graphql/Service.ts b/backend/graphql/Service.ts index 22b5b0f22..5d2505652 100644 --- a/backend/graphql/Service.ts +++ b/backend/graphql/Service.ts @@ -64,7 +64,7 @@ export const ServiceMutations = extendType({ resolve: async (_, args, ctx) => { const { url, name } = args - return await ctx.prisma.service.create({ + return ctx.prisma.service.create({ data: { url, name, diff --git a/backend/graphql/StudyModule/model.ts b/backend/graphql/StudyModule/model.ts index 0e89b943a..8feffe8f6 100644 --- a/backend/graphql/StudyModule/model.ts +++ b/backend/graphql/StudyModule/model.ts @@ -17,10 +17,9 @@ export const StudyModule = objectType({ t.model.updated_at() t.model.study_module_translations() - // @ts-ignore: false error t.string("description") - t.list.field("courses", { + t.list.nonNull.field("courses", { type: "Course", args: { orderBy: arg({ type: "CourseOrderByInput" }), diff --git a/backend/graphql/StudyModule/queries.ts b/backend/graphql/StudyModule/queries.ts index 78caef1bd..2371e73da 100644 --- a/backend/graphql/StudyModule/queries.ts +++ b/backend/graphql/StudyModule/queries.ts @@ -83,7 +83,7 @@ export const StudyModuleQueries = extendType({ ordering: true, }) - t.list.field("study_modules", { + t.list.nonNull.field("study_modules", { type: "StudyModule", args: { orderBy: arg({ type: "StudyModuleOrderByInput" }), diff --git a/backend/graphql/User/model.ts b/backend/graphql/User/model.ts index 2a98ba0cd..2141ed06e 100644 --- a/backend/graphql/User/model.ts +++ b/backend/graphql/User/model.ts @@ -125,7 +125,7 @@ export const User = objectType({ }, }) - t.field("project_completion", { + t.nonNull.field("project_completion", { type: "Boolean", args: { course_id: nullable(idArg()), @@ -271,7 +271,7 @@ export const User = objectType({ }, }) - t.list.field("exercise_completions", { + t.list.nonNull.field("exercise_completions", { type: "ExerciseCompletion", args: { includeDeletedExercises: nullable(booleanArg()), @@ -294,7 +294,7 @@ export const User = objectType({ }, }) - t.list.field("user_course_summary", { + t.list.nonNull.field("user_course_summary", { type: "UserCourseSummary", args: { includeDeletedExercises: booleanArg(), diff --git a/backend/graphql/UserCourseProgress.ts b/backend/graphql/UserCourseProgress.ts index 168626976..6491f1844 100644 --- a/backend/graphql/UserCourseProgress.ts +++ b/backend/graphql/UserCourseProgress.ts @@ -93,7 +93,7 @@ export const UserCourseProgress = objectType({ }, }) - t.field("exercise_progress", { + t.nonNull.field("exercise_progress", { type: "ExerciseProgress", resolve: async ({ course_id, user_id, n_points, max_points }, _, ctx) => { if (!course_id) { @@ -175,7 +175,7 @@ export const UserCourseProgressQueries = extendType({ }) // FIXME: (?) broken until the nexus json thing is fixed or smth - t.list.field("userCourseProgresses", { + t.list.nonNull.field("userCourseProgresses", { type: "UserCourseProgress", args: { user_id: idArg(), diff --git a/backend/graphql/UserCourseSummary.ts b/backend/graphql/UserCourseSummary.ts index d74a8b54f..bc85a929a 100644 --- a/backend/graphql/UserCourseSummary.ts +++ b/backend/graphql/UserCourseSummary.ts @@ -58,7 +58,7 @@ export const UserCourseSummary = objectType({ }, }) - t.list.field("user_course_service_progresses", { + t.nonNull.list.nonNull.field("user_course_service_progresses", { type: "UserCourseServiceProgress", resolve: async ( { user: { id: user_id }, course: { id: course_id } }, @@ -81,7 +81,7 @@ export const UserCourseSummary = objectType({ }, }) - t.list.field("exercise_completions", { + t.list.nonNull.field("exercise_completions", { type: "ExerciseCompletion", args: { includeDeletedExercises: booleanArg(), diff --git a/backend/graphql/UserOrganization/model.ts b/backend/graphql/UserOrganization/model.ts index ba4f2ff43..b2320a791 100644 --- a/backend/graphql/UserOrganization/model.ts +++ b/backend/graphql/UserOrganization/model.ts @@ -17,7 +17,7 @@ export const UserOrganization = objectType({ t.model.organizational_email() t.model.organizational_identifier() - t.nonNull.list.field("user_organization_join_confirmations", { + t.list.nonNull.field("user_organization_join_confirmations", { type: "UserOrganizationJoinConfirmation", resolve: async ({ id }, _, ctx) => { return ctx.prisma.userOrganization diff --git a/backend/graphql/UserOrganization/queries.ts b/backend/graphql/UserOrganization/queries.ts index 8fae3eaa7..c8be10611 100644 --- a/backend/graphql/UserOrganization/queries.ts +++ b/backend/graphql/UserOrganization/queries.ts @@ -8,7 +8,7 @@ import { assertUserIdOnlyForAdmin } from "./helpers" export const UserOrganizationQueries = extendType({ type: "Query", definition(t) { - t.list.field("userOrganizations", { + t.list.nonNull.field("userOrganizations", { type: "UserOrganization", args: { user_id: idArg(), diff --git a/backend/middlewares/fetchUser.ts b/backend/middlewares/fetchUser.ts index 4dbbf1b6a..f1a8c0f3a 100644 --- a/backend/middlewares/fetchUser.ts +++ b/backend/middlewares/fetchUser.ts @@ -20,7 +20,7 @@ export const moocfiAuthPlugin = () => next: Function, ) => { if (ctx.userDetails || ctx.organization) { - return await next(root, args, ctx, info) + return next(root, args, ctx, info) } const rawToken = ctx.req?.headers?.authorization // connection? @@ -33,7 +33,7 @@ export const moocfiAuthPlugin = () => await setContextUser(ctx, rawToken) } - return await next(root, args, ctx, info) + return next(root, args, ctx, info) } }, }) diff --git a/backend/middlewares/logger.ts b/backend/middlewares/logger.ts index cd7c48a92..10436a0dd 100644 --- a/backend/middlewares/logger.ts +++ b/backend/middlewares/logger.ts @@ -22,7 +22,7 @@ export const loggerPlugin = () => ) } - return await next(root, args, ctx, info) + return next(root, args, ctx, info) } }, }) diff --git a/backend/middlewares/newrelic.ts b/backend/middlewares/newrelic.ts index 17df6e4a8..058ba3ada 100644 --- a/backend/middlewares/newrelic.ts +++ b/backend/middlewares/newrelic.ts @@ -14,13 +14,13 @@ export const newRelicPlugin = () => ) } - const result = await next(root, args, ctx, info) + const result = await next(root, args, ctx, info) // NOSONAR can be async, even if sonar doesn't pick it up return result } catch (error) { newrelic.noticeError(error) - return await next(root, args, ctx, info) + return next(root, args, ctx, info) } } }, diff --git a/backend/middlewares/sentry.ts b/backend/middlewares/sentry.ts index c062857f4..80ac16acd 100644 --- a/backend/middlewares/sentry.ts +++ b/backend/middlewares/sentry.ts @@ -9,15 +9,9 @@ export const sentryPlugin = () => plugin({ name: "SentryPlugin", onCreateFieldResolver(config) { - return async ( - root: any, - args: Record, - ctx: Context, - info: any, - next: Function, - ) => { + return async (root, args, ctx: Context, info, next) => { try { - const result = await next(root, args, ctx, info) + const result: any = await next(root, args, ctx, info) // NOSONAR: can be async, even if sonar doesn't pick it up return result } catch (error) { diff --git a/backend/services/redis.ts b/backend/services/redis.ts index 8bdba9e14..7ae5dcac6 100644 --- a/backend/services/redis.ts +++ b/backend/services/redis.ts @@ -74,7 +74,7 @@ const getRedisClient = (): typeof redisClient => { * @returns */ export async function redisify( - fn: ((...props: any[]) => Promise | T) | Promise, + fn: ((...args: any[]) => Promise | T) | Promise, options: { prefix: string expireTime: number @@ -128,7 +128,7 @@ export async function redisify( } catch (e1) { try { if (!resolveSuccess) { - return await resolveValue() + value = await resolveValue() } return value } catch (e2) { diff --git a/frontend/components/Dashboard/Editor/Course/serialization.ts b/frontend/components/Dashboard/Editor/Course/serialization.ts index fb7fdb124..58089dbe0 100644 --- a/frontend/components/Dashboard/Editor/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor/Course/serialization.ts @@ -4,6 +4,7 @@ import { DateTime } from "luxon" import { initialValues } from "./form-validation" import { CourseFormValues, CourseTranslationFormValues } from "./types" +import notEmpty from "/util/notEmpty" import { CourseCreateArg, @@ -178,7 +179,7 @@ export const fromCourseForm = ({ course_translation.open_university_course_link.course_code.trim(), } }) - .filter((v) => !!v) + .filter(notEmpty) const study_modules = Object.keys(values.study_modules || {}) .filter((key) => values?.study_modules?.[key]) diff --git a/frontend/components/Dashboard/Editor2/Course/serialization.ts b/frontend/components/Dashboard/Editor2/Course/serialization.ts index 72282a7e3..31a3ffe1a 100644 --- a/frontend/components/Dashboard/Editor2/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor2/Course/serialization.ts @@ -3,6 +3,7 @@ import { DateTime } from "luxon" import { initialValues } from "./form-validation" import { CourseFormValues, CourseTranslationFormValues } from "./types" +import notEmpty from "/util/notEmpty" import { CourseCreateArg, @@ -220,7 +221,7 @@ export const fromCourseForm = ({ "", } }) - .filter((v) => !!v) + .filter(notEmpty) const study_modules = Object.keys(values.study_modules || {}) .filter((key) => values?.study_modules?.[key]) diff --git a/frontend/components/NewLayout/Courses/Catalogue.tsx b/frontend/components/NewLayout/Courses/Catalogue.tsx new file mode 100644 index 000000000..9e49b9aec --- /dev/null +++ b/frontend/components/NewLayout/Courses/Catalogue.tsx @@ -0,0 +1,23 @@ +import styled from "@emotion/styled" + +import CourseGrid from "./CourseGrid" +import CoursesTranslations from "/translations/_new/courses" +import { useTranslator } from "/util/useTranslator" + +const Container = styled.div` + display: grid; + justify-items: center; +` + +function Catalogue() { + const t = useTranslator(CoursesTranslations) + + return ( + +

{t("coursesHeader")}

+ +
+ ) +} + +export default Catalogue diff --git a/frontend/components/NewLayout/Courses/CourseCard.tsx b/frontend/components/NewLayout/Courses/CourseCard.tsx new file mode 100644 index 000000000..e9eb577a8 --- /dev/null +++ b/frontend/components/NewLayout/Courses/CourseCard.tsx @@ -0,0 +1,179 @@ +import styled from "@emotion/styled" +import { Button } from "@mui/material" + +import OutboundLink from "/components/OutboundLink" +import BannerImage from "/static/images/homeBackground.jpg" +import SponsorLogo from "/static/images/new/components/courses/f-secure_logo.png" + +import { CourseFieldsFragment } from "/graphql/generated" + +const colorSchemes = { + csb: ["#020024", "#090979", "#00d7ff"], + programming: ["#1b0024", "#791779", "#ff00e2"], + cloud: ["#160a01", "#a35e27", "#ff7900"], + ai: ["#161601", "#a3a127", "#fffa00"], +} + +const Container = styled.div` + border: 1px solid rgba(236, 236, 236, 1); + box-sizing: border-box; + box-shadow: 3px 3px 4px rgba(88, 89, 91, 0.25); + border-radius: 0.5rem; + &:nth-child(n) { + background: linear-gradient( + 90deg, + ${colorSchemes["cloud"][0]} 0%, + ${colorSchemes["cloud"][1]} 35%, + ${colorSchemes["cloud"][2]} 100% + ); + } + &:nth-child(2n) { + background: linear-gradient( + 90deg, + ${colorSchemes["programming"][0]} 0%, + ${colorSchemes["programming"][1]} 35%, + ${colorSchemes["programming"][2]} 100% + ); + } + &:nth-child(3n) { + background: linear-gradient( + 90deg, + ${colorSchemes["csb"][0]} 0%, + ${colorSchemes["csb"][1]} 35%, + ${colorSchemes["csb"][2]} 100% + ); + } + &:nth-child(4n) { + background: linear-gradient( + 90deg, + ${colorSchemes["ai"][0]} 0%, + ${colorSchemes["ai"][1]} 35%, + ${colorSchemes["ai"][2]} 100% + ); + } + &:nth-child(5n) { + background: url(${BannerImage}); + } +` + +const TitleContainer = styled.div` + display: grid; + grid-gap: 1rem; + padding: 1rem 1.5rem 0.5rem 1.5rem; + grid-template-columns: 67% 33%; + grid-template-rows: 100%; +` + +const ContentContainer = styled.div` + display: grid; + grid-gap: 1rem 2rem; + padding: 0.5rem 1.5rem 0.1rem 1.5rem; + grid-template-columns: 67% 33%; + grid-template-rows: 67% 33%; + background: rgba(255, 255, 255, 1); +` + +const Title = styled.div` + font-weight: bold; + text-align: center; + background: rgba(255, 255, 255, 1); + padding: 0.5rem; + border-radius: 0.2rem; +` + +const Sponsor = styled.img` + max-width: 9rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 1); + padding: 1rem; +` + +const Description = styled.div`` + +const Schedule = styled.div`` + +const Details = styled.div`` + +const Link = styled(OutboundLink)` + align-self: end; + margin: 1rem; +` + +const BottomLeftContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +const Tags = styled.div`` + +const Tag = styled(Button)` + border-radius: 2rem; + background-color: #378170 !important; + border-color: #378170 !important; + color: #fff !important; + font-weight: bold; + margin: 0.1rem; +` + +interface CourseCardProps { + course?: CourseFieldsFragment | null + tags?: string[] +} + +function CourseCard({ course, tags }: CourseCardProps) { + return course ? ( + + + {course?.name} + + + {course?.description} +
+ {course.ects && ( + <> + ~{parseInt(course.ects) * 27} ({course.ects}) + + )} + Helsingin yliopisto + +
+ + + {course.status}{" "} + {course?.end_date ? ( + <> + Aikataulutettu +
+ {course?.start_date} - {course?.end_date} + + ) : ( + <>Aikatauluton + )} +
+
+ + Näytä kurssi + + + {tags?.map((tag) => ( + + {tag} + + ))} + +
+
+ ) : ( + + + loading... + + + loading... + + + ) +} + +export default CourseCard diff --git a/frontend/components/NewLayout/Courses/CourseGrid.tsx b/frontend/components/NewLayout/Courses/CourseGrid.tsx new file mode 100644 index 000000000..7a1e3d8b2 --- /dev/null +++ b/frontend/components/NewLayout/Courses/CourseGrid.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from "react" + +import { useRouter } from "next/router" + +import { useQuery } from "@apollo/client" +import styled from "@emotion/styled" +import { Button, TextField } from "@mui/material" + +import CourseCard from "./CourseCard" +import CommonTranslations from "/translations/common" +import { mapNextLanguageToLocaleCode } from "/util/moduleFunctions" +import { useTranslator } from "/util/useTranslator" + +import { CoursesDocument } from "/graphql/generated" + +const Container = styled.div` + display: grid; + max-width: 1200px; +` + +const CardContainer = styled.div` + display: grid; + grid-gap: 2rem; + grid-template-columns: 50% 50%; +` + +const SearchBar = styled(TextField)` + margin: 0.5rem 0; +` + +const Filters = styled.div` + margin: 0 0 1rem 1rem; + display: flex; +` + +const FilterLabel = styled.div` + align-self: center; + margin-right: 1rem; +` + +const TagButton = styled(Button)` + border-radius: 2rem; + margin: 0 0.2rem; + font-weight: bold; + border-width: 0.15rem; +` + +function CourseGrid() { + const t = useTranslator(CommonTranslations) + const { locale = "fi" } = useRouter() + const language = mapNextLanguageToLocaleCode(locale) + const { loading, data } = useQuery(CoursesDocument, { + variables: { language }, + }) + const [searchString, setSearchString] = useState("") + const [tags, setTags] = useState([]) + const [activeTags, setActiveTags] = useState([]) + const [hardcodedTags, setHardcodedTags] = useState<{ + [key: string]: string[] + }>({}) + + // TODO: set tags on what tags are found from courses in db? or just do a hard-coded list of tags? + + useEffect(() => { + setTags([ + "beginner", + "intermediate", + "pro", + "AI", + "programming", + "cloud", + "cyber security", + ]) + + const hardcoded: { [key: string]: string[] } = {} + data?.courses?.map((course) => + course?.slug != null + ? (hardcoded[course?.slug] = [...tags] + .sort(() => 0.5 - Math.random()) + .slice(0, 4)) + : "undefined", + ) + setHardcodedTags(hardcoded) + }, []) + + const handleClick = (tag: string) => { + if (activeTags.includes(tag)) { + setActiveTags(activeTags.filter((t) => t !== tag)) + } else { + setActiveTags([...activeTags, tag]) + } + } + + return ( + + ) => + setSearchString(e.target.value) + } + /> + + {t("filter")}: + {tags.map((tag) => ( + handleClick(tag)} + size="small" + > + {tag} + + ))} + + {loading ? ( + + + + + + + ) : ( + + {data?.courses && + data.courses + .filter( + (course) => + (course?.name + .toLowerCase() + .includes(searchString.toLowerCase()) || + course?.description + ?.toLowerCase() + .includes(searchString.toLowerCase())) && + (activeTags.length > 0 + ? activeTags.every((tag) => + hardcodedTags[course?.slug].includes(tag), + ) + : true), + ) + .map((course) => ( + + ))} + + )} + + ) +} + +export default CourseGrid diff --git a/frontend/components/NewLayout/Courses/HeroSection.tsx b/frontend/components/NewLayout/Courses/HeroSection.tsx new file mode 100644 index 000000000..508e6f1a1 --- /dev/null +++ b/frontend/components/NewLayout/Courses/HeroSection.tsx @@ -0,0 +1,85 @@ +import Image from "next/image" + +import styled from "@emotion/styled" + +import CoursesTranslations from "/translations/_new/courses" +import { useTranslator } from "/util/useTranslator" + +const Container = styled.div` + display: grid; + justify-items: center; +` + +const ItemContainer = styled.div` + display: flex; + align-items: center; +` + +const Item = styled.div` + margin: 2rem; + display: grid; + grid-gap: 2rem; + justify-items: center; +` + +function HeroSection() { + const t = useTranslator(CoursesTranslations) + + return ( + +

{t("heroSectionHeader")}

+ + + Not Found + {t("heroSectionDescription1")} + + + Not Found + + + Not Found + {t("heroSectionDescription2")} + + + Not Found + + + Not Found + {t("heroSectionDescription3")} + + +
+ ) +} + +export default HeroSection diff --git a/frontend/graphql/generated/index.ts b/frontend/graphql/generated/index.ts index e0a23c3da..e3ddd3895 100644 --- a/frontend/graphql/generated/index.ts +++ b/frontend/graphql/generated/index.ts @@ -16,7 +16,7 @@ export type MakeOptional = Omit & { export type MakeMaybe = Omit & { [SubKey in K]: Maybe } -// Generated on 2022-09-27T12:44:49+03:00 +// Generated on 2022-10-20T16:19:27+03:00 /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { @@ -111,8 +111,8 @@ export type Completion = { email: Scalars["String"] grade: Maybe id: Scalars["String"] - project_completion: Maybe - registered: Maybe + project_completion: Scalars["Boolean"] + registered: Scalars["Boolean"] student_number: Maybe tier: Maybe updated_at: Maybe @@ -169,7 +169,7 @@ export type Course = { automatic_completions_eligible_for_ects: Maybe completion_email: Maybe completion_email_id: Maybe - completions: Maybe>> + completions: Maybe> completions_handled_by: Maybe completions_handled_by_id: Maybe course_aliases: Array @@ -183,7 +183,7 @@ export type Course = { ects: Maybe end_date: Maybe exercise_completions_needed: Maybe - exercises: Maybe>> + exercises: Maybe> handles_completions_for: Array has_certificate: Maybe hidden: Maybe @@ -313,12 +313,10 @@ export type CourseCreateArg = { base64?: InputMaybe completion_email_id?: InputMaybe completions_handled_by?: InputMaybe - course_aliases?: InputMaybe>> + course_aliases?: InputMaybe> course_stats_email_id?: InputMaybe - course_translations?: InputMaybe< - Array> - > - course_variants?: InputMaybe>> + course_translations?: InputMaybe> + course_variants?: InputMaybe> ects?: InputMaybe end_date?: InputMaybe exercise_completions_needed?: InputMaybe @@ -328,7 +326,7 @@ export type CourseCreateArg = { name?: InputMaybe new_photo?: InputMaybe open_university_registration_links?: InputMaybe< - Array> + Array > order?: InputMaybe photo?: InputMaybe @@ -340,14 +338,14 @@ export type CourseCreateArg = { status?: InputMaybe study_module_order?: InputMaybe study_module_start_point?: InputMaybe - study_modules?: InputMaybe>> + study_modules?: InputMaybe> support_email?: InputMaybe teacher_in_charge_email: Scalars["String"] teacher_in_charge_name: Scalars["String"] tier?: InputMaybe upcoming_active_link?: InputMaybe user_course_settings_visibilities?: InputMaybe< - Array> + Array > } @@ -493,12 +491,10 @@ export type CourseUpsertArg = { base64?: InputMaybe completion_email_id?: InputMaybe completions_handled_by?: InputMaybe - course_aliases?: InputMaybe>> + course_aliases?: InputMaybe> course_stats_email_id?: InputMaybe - course_translations?: InputMaybe< - Array> - > - course_variants?: InputMaybe>> + course_translations?: InputMaybe> + course_variants?: InputMaybe> delete_photo?: InputMaybe ects?: InputMaybe end_date?: InputMaybe @@ -511,7 +507,7 @@ export type CourseUpsertArg = { new_photo?: InputMaybe new_slug?: InputMaybe open_university_registration_links?: InputMaybe< - Array> + Array > order?: InputMaybe photo?: InputMaybe @@ -523,14 +519,14 @@ export type CourseUpsertArg = { status?: InputMaybe study_module_order?: InputMaybe study_module_start_point?: InputMaybe - study_modules?: InputMaybe>> + study_modules?: InputMaybe> support_email?: InputMaybe teacher_in_charge_email: Scalars["String"] teacher_in_charge_name: Scalars["String"] tier?: InputMaybe upcoming_active_link?: InputMaybe user_course_settings_visibilities?: InputMaybe< - Array> + Array > } @@ -641,7 +637,7 @@ export type Exercise = { created_at: Maybe custom_id: Scalars["String"] deleted: Maybe - exercise_completions: Maybe>> + exercise_completions: Maybe> id: Scalars["String"] max_points: Maybe name: Maybe @@ -756,7 +752,7 @@ export type Mutation = { addExercise: Maybe addExerciseCompletion: Maybe addImage: Maybe - addManualCompletion: Maybe>> + addManualCompletion: Maybe> addOpenUniversityRegistrationLink: Maybe addOrganization: Maybe addService: Maybe @@ -781,7 +777,7 @@ export type Mutation = { deleteStudyModuleTranslation: Maybe deleteUserOrganization: Maybe recheckCompletions: Maybe - registerCompletion: Maybe + registerCompletion: Scalars["String"] requestNewUserOrganizationJoinConfirmation: Maybe updateAbEnrollment: Maybe updateAbStudy: Maybe @@ -1251,12 +1247,15 @@ export type OrganizationOrderByInput = { email?: InputMaybe hidden?: InputMaybe id?: InputMaybe + join_organization_email_template_id?: InputMaybe logo_content_type?: InputMaybe logo_file_name?: InputMaybe logo_file_size?: InputMaybe logo_updated_at?: InputMaybe phone?: InputMaybe pinned?: InputMaybe + required_confirmation?: InputMaybe + required_organization_email?: InputMaybe secret_key?: InputMaybe slug?: InputMaybe tmc_created_at?: InputMaybe @@ -1321,45 +1320,45 @@ export type Progress = { course: Maybe user: Maybe user_course_progress: Maybe - user_course_service_progresses: Maybe>> + user_course_service_progresses: Maybe> } export type Query = { __typename?: "Query" - completions: Maybe>> + completions: Maybe> completionsPaginated: Maybe completionsPaginated_type: Maybe course: Maybe courseAliases: Array - courseOrganizations: Maybe>> - courseTranslations: Maybe>> + courseOrganizations: Maybe> + courseTranslations: Maybe> courseVariant: Maybe - courseVariants: Maybe>> - course_exists: Maybe - courses: Maybe>> + courseVariants: Maybe> + course_exists: Scalars["Boolean"] + courses: Maybe> currentUser: Maybe email_template: Maybe - email_templates: Maybe>> + email_templates: Maybe> exercise: Maybe exerciseCompletion: Maybe exerciseCompletions: Array exercises: Array - handlerCourses: Maybe>> + handlerCourses: Maybe> openUniversityRegistrationLink: Maybe openUniversityRegistrationLinks: Array /** Get organization by id or slug. Admins can also query hidden/disabled courses. Fields that can be queried is more limited on normal users. */ organization: Maybe - organizations: Maybe>> - registeredCompletions: Maybe>> + organizations: Maybe> + registeredCompletions: Maybe> service: Maybe services: Array studyModuleTranslations: Array study_module: Maybe study_module_exists: Maybe - study_modules: Maybe>> + study_modules: Maybe> user: Maybe userCourseProgress: Maybe - userCourseProgresses: Maybe>> + userCourseProgresses: Maybe> userCourseServiceProgress: Maybe userCourseServiceProgresses: Array userCourseSetting: Maybe @@ -1367,7 +1366,7 @@ export type Query = { userCourseSettings: Maybe userDetailsContains: Maybe userOrganizationJoinConfirmation: Maybe - userOrganizations: Maybe>> + userOrganizations: Maybe> users: Array } @@ -1485,7 +1484,7 @@ export type QueryopenUniversityRegistrationLinksArgs = { } export type QueryorganizationArgs = { - disaled?: InputMaybe + disabled?: InputMaybe hidden?: InputMaybe id?: InputMaybe slug?: InputMaybe @@ -1717,7 +1716,7 @@ export type StringNullableFilter = { export type StudyModule = { __typename?: "StudyModule" - courses: Maybe>> + courses: Maybe> created_at: Maybe description: Maybe id: Scalars["String"] @@ -1819,14 +1818,14 @@ export type User = { created_at: Maybe email: Scalars["String"] email_deliveries: Array - exercise_completions: Maybe>> + exercise_completions: Maybe> first_name: Maybe id: Scalars["String"] last_name: Maybe organizations: Array progress: Progress progresses: Maybe> - project_completion: Maybe + project_completion: Scalars["Boolean"] real_student_number: Maybe research_consent: Maybe student_number: Maybe @@ -1836,7 +1835,7 @@ export type User = { user_course_progressess: Maybe user_course_service_progresses: Maybe> user_course_settings: Array - user_course_summary: Maybe>> + user_course_summary: Maybe> user_organizations: Array username: Scalars["String"] verified_users: Array @@ -1937,7 +1936,7 @@ export type UserCourseProgress = { course: Maybe course_id: Maybe created_at: Maybe - exercise_progress: Maybe + exercise_progress: ExerciseProgress extra: Maybe id: Scalars["String"] max_points: Maybe @@ -2040,10 +2039,10 @@ export type UserCourseSummary = { completion: Maybe course: Course course_id: Scalars["ID"] - exercise_completions: Maybe>> + exercise_completions: Maybe> user: User user_course_progress: Maybe - user_course_service_progresses: Maybe>> + user_course_service_progresses: Array user_id: Scalars["ID"] } @@ -2083,13 +2082,9 @@ export type UserOrganization = { updated_at: Maybe user: Maybe user_id: Maybe - user_organization_join_confirmations: Array -} - -export type UserOrganizationuser_organization_join_confirmationsArgs = { - cursor?: InputMaybe - skip?: InputMaybe - take?: InputMaybe + user_organization_join_confirmations: Maybe< + Array + > } export type UserOrganizationJoinConfirmation = { @@ -2110,11 +2105,6 @@ export type UserOrganizationJoinConfirmation = { user_organization_id: Scalars["String"] } -export type UserOrganizationJoinConfirmationWhereUniqueInput = { - email_delivery_id?: InputMaybe - id?: InputMaybe -} - export type UserOrganizationWhereUniqueInput = { id?: InputMaybe } @@ -2171,8 +2161,8 @@ export type CompletionCoreFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null } @@ -2214,8 +2204,8 @@ export type CompletionDetailedFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null completions_registered: Array<{ @@ -2252,8 +2242,8 @@ export type CompletionDetailedFieldsWithCourseFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null course: { @@ -2313,8 +2303,8 @@ export type CompletionsQueryNodeFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null user: { @@ -2374,8 +2364,8 @@ export type CompletionsQueryConnectionFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null user: { @@ -2879,7 +2869,7 @@ export type ProgressCoreFieldsFragment = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -2891,7 +2881,7 @@ export type ProgressCoreFieldsFragment = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> | null } export type StudyModuleCoreFieldsFragment = { @@ -3004,7 +2994,7 @@ export type StudyModuleFieldsWithCoursesFragment = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type UserCoreFieldsFragment = { @@ -3075,7 +3065,7 @@ export type UserProgressesFieldsFragment = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -3087,7 +3077,7 @@ export type UserProgressesFieldsFragment = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> | null }> | null } @@ -3118,8 +3108,8 @@ export type UserOverviewFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null course: { @@ -3182,7 +3172,7 @@ export type UserCourseProgressCoreFieldsFragment = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } export type UserCourseServiceProgressCoreFieldsFragment = { @@ -3266,7 +3256,7 @@ export type StudentProgressesQueryNodeFieldsFragment = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -3278,7 +3268,7 @@ export type StudentProgressesQueryNodeFieldsFragment = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> | null } } | null } @@ -3326,7 +3316,7 @@ export type UserCourseSummaryCourseFieldsFragment = { section: number | null max_points: number | null deleted: boolean | null - } | null> | null + }> | null photo: { __typename?: "Image" id: string @@ -3363,7 +3353,7 @@ export type UserCourseSummaryCoreFieldsFragment = { section: number | null max_points: number | null deleted: boolean | null - } | null> | null + }> | null photo: { __typename?: "Image" id: string @@ -3395,7 +3385,7 @@ export type UserCourseSummaryCoreFieldsFragment = { exercise_completion_id: string | null value: string }> - } | null> | null + }> | null user_course_progress: { __typename?: "UserCourseProgress" id: string @@ -3411,7 +3401,7 @@ export type UserCourseSummaryCoreFieldsFragment = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -3423,7 +3413,7 @@ export type UserCourseSummaryCoreFieldsFragment = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> completion: { __typename?: "Completion" id: string @@ -3437,8 +3427,8 @@ export type UserCourseSummaryCoreFieldsFragment = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null completions_registered: Array<{ @@ -3562,7 +3552,7 @@ export type UserOrganizationWithUserOrganizationJoinConfirmationFieldsFragment = created_at: any | null updated_at: any | null } | null - }> + }> | null organization: { __typename?: "Organization" id: string @@ -3628,8 +3618,8 @@ export type AddManualCompletionMutation = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null user: { @@ -3645,7 +3635,7 @@ export type AddManualCompletionMutation = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type AddCourseMutationVariables = Exact<{ @@ -4164,7 +4154,7 @@ export type AddUserOrganizationMutation = { created_at: any | null updated_at: any | null } | null - }> + }> | null organization: { __typename?: "Organization" id: string @@ -4248,7 +4238,7 @@ export type UpdateUserOrganizationOrganizationalMailMutation = { created_at: any | null updated_at: any | null } | null - }> + }> | null organization: { __typename?: "Organization" id: string @@ -4325,7 +4315,7 @@ export type ConfirmUserOrganizationJoinMutation = { created_at: any | null updated_at: any | null } | null - }> + }> | null organization: { __typename?: "Organization" id: string @@ -4453,8 +4443,8 @@ export type PaginatedCompletionsQuery = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null user: { @@ -4526,8 +4516,8 @@ export type PaginatedCompletionsPreviousPageQuery = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null user: { @@ -4622,7 +4612,7 @@ export type CoursesQuery = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type EditorCoursesQueryVariables = Exact<{ @@ -4710,7 +4700,7 @@ export type EditorCoursesQuery = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type CourseFromSlugQueryVariables = Exact<{ @@ -4765,7 +4755,7 @@ export type CourseEditorOtherCoursesQuery = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type HandlerCoursesQueryVariables = Exact<{ [key: string]: never }> @@ -4780,7 +4770,7 @@ export type HandlerCoursesQuery = { ects: string | null created_at: any | null updated_at: any | null - } | null> | null + }> | null } export type CourseEditorDetailsQueryVariables = Exact<{ @@ -4925,7 +4915,7 @@ export type EmailTemplateEditorCoursesQuery = { created_at: any | null updated_at: any | null } | null - } | null> | null + }> | null } export type CourseDashboardQueryVariables = Exact<{ @@ -4987,7 +4977,7 @@ export type EmailTemplatesQuery = { template_type: string | null created_at: any | null updated_at: any | null - } | null> | null + }> | null } export type EmailTemplateQueryVariables = Exact<{ @@ -5035,7 +5025,7 @@ export type OrganizationsQuery = { name: string information: string | null }> - } | null> | null + }> | null } export type OrganizationQueryVariables = Exact<{ @@ -5083,7 +5073,7 @@ export type StudyModulesQuery = { id: string slug: string name: string - } | null> | null + }> | null } export type EditorStudyModulesQueryVariables = Exact<{ [key: string]: never }> @@ -5109,7 +5099,7 @@ export type EditorStudyModulesQuery = { created_at: any | null updated_at: any | null }> - } | null> | null + }> | null } export type EditorStudyModuleDetailsQueryVariables = Exact<{ @@ -5136,7 +5126,7 @@ export type EditorStudyModuleDetailsQuery = { ects: string | null created_at: any | null updated_at: any | null - } | null> | null + }> | null study_module_translations: Array<{ __typename?: "StudyModuleTranslation" id: string @@ -5247,7 +5237,7 @@ export type UserSummaryQuery = { section: number | null max_points: number | null deleted: boolean | null - } | null> | null + }> | null photo: { __typename?: "Image" id: string @@ -5279,7 +5269,7 @@ export type UserSummaryQuery = { exercise_completion_id: string | null value: string }> - } | null> | null + }> | null user_course_progress: { __typename?: "UserCourseProgress" id: string @@ -5295,7 +5285,7 @@ export type UserSummaryQuery = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -5307,7 +5297,7 @@ export type UserSummaryQuery = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> completion: { __typename?: "Completion" id: string @@ -5321,8 +5311,8 @@ export type UserSummaryQuery = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null completions_registered: Array<{ @@ -5345,7 +5335,7 @@ export type UserSummaryQuery = { honors: boolean | null } | null } | null - } | null> | null + }> | null } | null } @@ -5380,8 +5370,8 @@ export type CurrentUserOverviewQuery = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null course: { @@ -5463,8 +5453,8 @@ export type UserOverviewQuery = { tier: number | null grade: string | null eligible_for_ects: boolean | null - project_completion: boolean | null - registered: boolean | null + project_completion: boolean + registered: boolean created_at: any | null updated_at: any | null course: { @@ -5557,7 +5547,7 @@ export type CurrentUserProgressesQuery = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -5569,7 +5559,7 @@ export type CurrentUserProgressesQuery = { created_at: any | null updated_at: any | null service: { __typename?: "Service"; name: string; id: string } | null - } | null> | null + }> | null }> | null } | null } @@ -5715,7 +5705,7 @@ export type ExportUserCourseProgressesQuery = { country: string | null language: string | null } | null - } | null> | null + }> | null } export type StudentProgressesQueryVariables = Exact<{ @@ -5782,7 +5772,7 @@ export type StudentProgressesQuery = { __typename?: "ExerciseProgress" total: number | null exercises: number | null - } | null + } } | null user_course_service_progresses: Array<{ __typename?: "UserCourseServiceProgress" @@ -5798,7 +5788,7 @@ export type StudentProgressesQuery = { name: string id: string } | null - } | null> | null + }> | null } } | null } | null @@ -5893,7 +5883,7 @@ export type CurrentUserUserOrganizationsQuery = { created_at: any | null updated_at: any | null } | null - }> + }> | null organization: { __typename?: "Organization" id: string @@ -5958,7 +5948,7 @@ export type UserOrganizationsQuery = { information: string | null }> } | null - } | null> | null + }> | null } export type UserOrganizationJoinConfirmationQueryVariables = Exact<{ diff --git a/frontend/graphql/queries/courses.ts b/frontend/graphql/queries/courses.ts deleted file mode 100644 index 4bee0ca3f..000000000 --- a/frontend/graphql/queries/courses.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { gql } from "@apollo/client" - -export const AllCoursesQuery = gql` - query AllCourses($language: String) { - courses(orderBy: { order: asc }, language: $language) { - id - slug - name - order - study_module_order - photo { - id - compressed - uncompressed - } - promote - status - start_point - study_module_start_point - hidden - description - link - upcoming_active_link - start_date - end_date - study_modules { - id - slug - } - course_translations { - id - language - name - } - user_course_settings_visibilities { - id - language - } - } - } -` - -export const AllEditorCoursesQuery = gql` - query AllEditorCourses( - $search: String - $hidden: Boolean - $handledBy: String - $status: [CourseStatus!] - ) { - courses( - orderBy: { name: asc } - search: $search - hidden: $hidden - handledBy: $handledBy - status: $status - ) { - id - name - slug - order - status - hidden - tier - instructions - completions_handled_by { - id - } - start_date - end_date - support_email - teacher_in_charge_email - teacher_in_charge_name - photo { - id - compressed - uncompressed - } - course_translations { - id - language - name - } - course_variants { - id - slug - description - } - course_aliases { - id - course_code - } - user_course_settings_visibilities { - id - language - } - upcoming_active_link - } - currentUser { - id - administrator - } - } -` - -export const CheckSlugQuery = gql` - query CheckSlug($slug: String!) { - course(slug: $slug) { - id - slug - name - description - instructions - } - } -` - -export const CourseEditorStudyModuleQuery = gql` - query CourseEditorStudyModules { - study_modules { - id - name - slug - } - } -` -export const CourseEditorCoursesQuery = gql` - query CourseEditorCourses { - courses { - id - slug - name - course_translations { - id - name - language - } - photo { - id - name - original - original_mimetype - compressed - compressed_mimetype - uncompressed - uncompressed_mimetype - } - } - } -` - -export const HandlerCoursesQuery = gql` - query HandlerCourses { - handlerCourses { - id - slug - name - } - } -` - -export const CourseQuery = gql` - query CourseDetails($slug: String) { - course(slug: $slug) { - id - name - slug - ects - order - study_module_order - teacher_in_charge_name - teacher_in_charge_email - support_email - start_date - end_date - tier - photo { - id - compressed - compressed_mimetype - uncompressed - uncompressed_mimetype - } - promote - start_point - hidden - study_module_start_point - status - course_translations { - id - name - language - description - instructions - link - } - open_university_registration_links { - id - course_code - language - link - } - study_modules { - id - } - course_variants { - id - slug - description - } - course_aliases { - id - course_code - } - inherit_settings_from { - id - } - completions_handled_by { - id - } - has_certificate - user_course_settings_visibilities { - id - language - } - upcoming_active_link - automatic_completions - automatic_completions_eligible_for_ects - exercise_completions_needed - points_needed - } - } -` diff --git a/frontend/pages/_new/courses/index.tsx b/frontend/pages/_new/courses/index.tsx index de421995e..d6e0d4f51 100644 --- a/frontend/pages/_new/courses/index.tsx +++ b/frontend/pages/_new/courses/index.tsx @@ -1,5 +1,20 @@ +import styled from "@emotion/styled" + +import Catalogue from "/components/NewLayout/Courses/Catalogue" +import HeroSection from "/components/NewLayout/Courses/HeroSection" + +const Container = styled.div` + display: grid; + justify-content: center; +` + function Courses() { - return
courses
+ return ( + + + + + ) } export default Courses diff --git a/frontend/static/images/new/components/courses/f-secure_logo.png b/frontend/static/images/new/components/courses/f-secure_logo.png new file mode 100644 index 000000000..be7e32f47 Binary files /dev/null and b/frontend/static/images/new/components/courses/f-secure_logo.png differ diff --git a/frontend/static/images/new/components/courses/herosection_icon1.png b/frontend/static/images/new/components/courses/herosection_icon1.png new file mode 100644 index 000000000..f84b7fd54 Binary files /dev/null and b/frontend/static/images/new/components/courses/herosection_icon1.png differ diff --git a/frontend/static/images/new/components/courses/herosection_icon2.png b/frontend/static/images/new/components/courses/herosection_icon2.png new file mode 100644 index 000000000..cf23491de Binary files /dev/null and b/frontend/static/images/new/components/courses/herosection_icon2.png differ diff --git a/frontend/static/images/new/components/courses/herosection_icon3.png b/frontend/static/images/new/components/courses/herosection_icon3.png new file mode 100644 index 000000000..68d53052d Binary files /dev/null and b/frontend/static/images/new/components/courses/herosection_icon3.png differ diff --git a/frontend/static/images/new/components/courses/herosection_transition_icon.png b/frontend/static/images/new/components/courses/herosection_transition_icon.png new file mode 100644 index 000000000..2aeea6286 Binary files /dev/null and b/frontend/static/images/new/components/courses/herosection_transition_icon.png differ diff --git a/frontend/translations/_new/courses/en.json b/frontend/translations/_new/courses/en.json new file mode 100644 index 000000000..a9c13dd5f --- /dev/null +++ b/frontend/translations/_new/courses/en.json @@ -0,0 +1,7 @@ +{ + "heroSectionHeader": "Miten voin opiskella kursseja?", + "heroSectionDescription1": "Suorita yksittäisiä kursseja", + "heroSectionDescription2": "Opintokokonaisuuksien suorittaminen", + "heroSectionDescription3": "Hae suoritusmerkinnät opintoihisi", + "coursesHeader": "Kaikki kurssit" +} diff --git a/frontend/translations/_new/courses/fi.json b/frontend/translations/_new/courses/fi.json new file mode 100644 index 000000000..a9c13dd5f --- /dev/null +++ b/frontend/translations/_new/courses/fi.json @@ -0,0 +1,7 @@ +{ + "heroSectionHeader": "Miten voin opiskella kursseja?", + "heroSectionDescription1": "Suorita yksittäisiä kursseja", + "heroSectionDescription2": "Opintokokonaisuuksien suorittaminen", + "heroSectionDescription3": "Hae suoritusmerkinnät opintoihisi", + "coursesHeader": "Kaikki kurssit" +} diff --git a/frontend/translations/_new/courses/index.ts b/frontend/translations/_new/courses/index.ts new file mode 100644 index 000000000..d2f5b00a1 --- /dev/null +++ b/frontend/translations/_new/courses/index.ts @@ -0,0 +1,11 @@ +import en from "./en.json" +import fi from "./fi.json" +import { TranslationDictionary } from "/translations" + +export type CoursesTranslations = typeof en & typeof fi +const CoursesTranslations: TranslationDictionary = { + en, + fi, +} + +export default CoursesTranslations diff --git a/frontend/translations/common/en.json b/frontend/translations/common/en.json index d9568a0ba..21e404d06 100644 --- a/frontend/translations/common/en.json +++ b/frontend/translations/common/en.json @@ -52,6 +52,7 @@ "Ended": "Ended", "handledBy": "Completions handled by", "search": "Search...", + "filter": "Filter", "reset": "Reset", "showAll": "Show all", "hideAll": "Hide all", diff --git a/frontend/translations/common/fi.json b/frontend/translations/common/fi.json index 7e810645a..a3d99145f 100644 --- a/frontend/translations/common/fi.json +++ b/frontend/translations/common/fi.json @@ -52,6 +52,7 @@ "Ended": "Loppunut", "handledBy": "Suoritukset käsittelevä kurssi", "search": "Etsi...", + "filter": "Suodata", "reset": "Tyhjennä", "showAll": "Näytä kaikki", "hideAll": "Piilota kaikki", diff --git a/frontend/translations/common/se.json b/frontend/translations/common/se.json index 5cb89766a..4eec05863 100644 --- a/frontend/translations/common/se.json +++ b/frontend/translations/common/se.json @@ -52,6 +52,7 @@ "Ended": "Ended", "handledBy": "Handled by", "search": "Search...", + "filter": "Filter", "reset": "Reset", "showAll": "Show all", "hideAll": "Hide all", @@ -64,4 +65,4 @@ "ukraineHyText": "Support students and researchers affected by the invasion of Ukraine", "ukraineHyLinkText": "University of Helsinki Ukraine appeal", "noResults": "No results" -} \ No newline at end of file +}