diff --git a/api-gateway/src/main/resources/application-dev.yml b/api-gateway/src/main/resources/application-dev.yml index f1f81a97e4..a2098b8e0e 100644 --- a/api-gateway/src/main/resources/application-dev.yml +++ b/api-gateway/src/main/resources/application-dev.yml @@ -16,6 +16,10 @@ spring: client: provider: # example: https://github.com/wearearima/spring-boot-dex/blob/master/src/main/resources/application.properties + gitee: + authorization-uri: https://gitee.com/oauth/authorize + token-uri: https://gitee.com/oauth/token + user-info-uri: https://gitee.com/api/v5/user dex: authorization-uri: http://localhost:5556/dex/auth token-uri: http://localhost:5556/dex/token @@ -31,6 +35,15 @@ spring: # for more details. user-name-attribute: login registration: + gitee: + client-id: 6b7fc07ecdf7a12d9aa8e2aaf034743baa6f77e036dc22fe6455e02fcf51a851 + client-secret: 8908a8a4239c688af45a4db89ec27d4a7e7f5076dc3125b07b506de6414926a3 + client-name: Gitee + provider: gitee + redirect-uri: '${gateway.frontend.url}/{action}/oauth2/code/{registrationId}' + authorization-grant-type: authorization_code + scope: + - user_info dex: client-id: save-gateway-dev client-secret: 123test123 diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt index 05fdf875cf..838e13ff53 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt @@ -112,17 +112,10 @@ class App : ComponentWithScope() { requestModalHandler { userInfo = state.userInfo - withRouter { location, _ -> - if (state.userInfo?.isActive == false && !location.pathname.startsWith("/${FrontendRoutes.REGISTRATION.path}")) { - Navigate { - to = "/${FrontendRoutes.REGISTRATION.path}" - replace = false - } - } else if (state.userInfo?.isActive == true && location.pathname.startsWith("/${FrontendRoutes.REGISTRATION.path}")) { - Navigate { - to = "/${FrontendRoutes.PROJECTS.path}" - replace = false - } + if (state.userInfo?.isActive == false) { + Navigate { + to = "/${FrontendRoutes.REGISTRATION.path}" + replace = false } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/AvatarRenderers.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/AvatarRenderers.kt index 5fbb17aa15..27988a6ec8 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/AvatarRenderers.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/AvatarRenderers.kt @@ -5,6 +5,7 @@ package com.saveourtool.save.frontend.components.basic import com.saveourtool.save.entities.OrganizationDto +import com.saveourtool.save.frontend.utils.AVATAR_PROFILE_PLACEHOLDER import com.saveourtool.save.info.UserInfo import js.core.jso import react.CSSProperties @@ -18,11 +19,6 @@ import web.cssom.ClassName */ const val ORGANIZATION_AVATAR_PLACEHOLDER = "img/company.svg" -/** - * Placeholder for user avatar - */ -const val USER_AVATAR_PLACEHOLDER = "img/undraw_profile.svg" - /** * Render organization avatar or placeholder * @@ -51,7 +47,7 @@ fun ChildrenBuilder.renderAvatar( classes: String = "", link: String? = null, styleBuilder: CSSProperties.() -> Unit = {}, -) = renderAvatar(userInfo.avatar ?: USER_AVATAR_PLACEHOLDER, classes, link, styleBuilder) +) = renderAvatar(userInfo.avatar ?: AVATAR_PROFILE_PLACEHOLDER, classes, link, styleBuilder) private fun ChildrenBuilder.renderAvatar( avatarLink: String, diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/Forum.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/Forum.kt index 0afcccc982..36b3b68da0 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/Forum.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/Forum.kt @@ -185,7 +185,7 @@ private fun ChildrenBuilder.renderLeftColumn( rating: Long, color: String = "#f1f1f1", ) { - val (avatar, setAvatar) = useState(userAvatar?.let { "/api/$v1/avatar$it" } ?: "img/undraw_profile.svg") + val (avatar, setAvatar) = useState(userAvatar?.let { "/api/$v1/avatar$it" } ?: AVATAR_PROFILE_PLACEHOLDER) div { className = ClassName("input-group-prepend col-2") diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/modal/ModalStyles.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/modal/ModalStyles.kt index 914e50a02d..f15be7711e 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/modal/ModalStyles.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/modal/ModalStyles.kt @@ -6,7 +6,12 @@ import com.saveourtool.save.frontend.externals.modal.Styles import react.CSSProperties import kotlin.js.json -private val defaultOverlayProperties: CSSProperties = json("zIndex" to "1000").unsafeCast() +/** + * Maximum zIndex in the project, should be only used in modal windows + */ +internal const val MAX_Z_INDEX = 1000 + +private val defaultOverlayProperties: CSSProperties = json("zIndex" to MAX_Z_INDEX.toString()).unsafeCast() val defaultModalStyle = Styles( // make modal window occupy center of the screen diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt index 6a05923bb6..fed703e262 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt @@ -11,6 +11,7 @@ import com.saveourtool.save.info.UserInfo import com.saveourtool.save.v1 import com.saveourtool.save.validation.FrontendRoutes +import js.core.jso import react.* import react.dom.aria.* import react.dom.html.ReactHTML.a @@ -22,6 +23,7 @@ import react.dom.html.ReactHTML.span import react.dom.html.ReactHTML.ul import react.router.useNavigate import web.cssom.ClassName +import web.cssom.rem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,6 +32,13 @@ import kotlinx.coroutines.isActive val topBarUserField = topBarUserField() +@Suppress("MAGIC_NUMBER") +val logoSize: CSSProperties = + jso { + height = 2.5.rem + width = 2.5.rem + } + /** * [Props] of the top bar user field component */ @@ -91,20 +100,23 @@ private fun topBarUserField() = FC { props -> +(props.userInfo?.name.orEmpty()) } val globalRole = props.userInfo?.globalRole ?: Role.VIEWER - if (globalRole.isHigherOrEqualThan(Role.ADMIN)) { - small { - className = ClassName("text-gray-400 text-justify") - +globalRole.formattedName + small { + className = ClassName("text-gray-400 text-justify") + props.userInfo?.let { + if (globalRole.isHigherOrEqualThan(Role.ADMIN)) { + +"Super user" + } else { + +"User settings" + } } } } - props.userInfo?.avatar?.let { + props.userInfo?.let { userInfo -> img { className = ClassName("ml-2 align-self-center avatar avatar-user width-full border color-bg-default rounded-circle fas mr-2") - src = avatar - height = 45.0 - width = 45.0 + src = userInfo.avatar?.let { avatar } ?: AVATAR_PROFILE_PLACEHOLDER + style = logoSize onError = { setAvatar { AVATAR_PLACEHOLDER } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt index dcb44a96ee..14a80b92e6 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt @@ -208,7 +208,7 @@ class OrganizationView : AbstractView( isEditDisabled = true selfRole = highestRole usersInOrganization = users - avatar = organizationLoaded.avatar?.let { "/api/$v1/avatar$it" } ?: "img/undraw_profile.svg" + avatar = organizationLoaded.avatar?.let { "/api/$v1/avatar$it" } ?: AVATAR_PROFILE_PLACEHOLDER } urlAnalysis(OrganizationMenuBar, highestRole, organizationLoaded.canCreateContests) } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/RegistrationView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/RegistrationView.kt index 6b21dc9e2a..8d6a1a1f3d 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/RegistrationView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/RegistrationView.kt @@ -8,13 +8,13 @@ package com.saveourtool.save.frontend.components.views import com.saveourtool.save.frontend.components.inputform.InputTypes import com.saveourtool.save.frontend.components.inputform.inputTextFormRequired +import com.saveourtool.save.frontend.components.modal.MAX_Z_INDEX import com.saveourtool.save.frontend.http.postImageUpload import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.frontend.utils.classLoadingHandler import com.saveourtool.save.info.UserInfo import com.saveourtool.save.utils.AvatarType import com.saveourtool.save.v1 -import com.saveourtool.save.validation.FrontendRoutes import com.saveourtool.save.validation.isValidName import js.core.asList @@ -30,7 +30,9 @@ import react.dom.html.ReactHTML.input import react.dom.html.ReactHTML.label import react.dom.html.ReactHTML.main import react.dom.html.ReactHTML.span +import react.router.Navigate import web.cssom.ClassName +import web.cssom.ZIndex import web.cssom.rem import web.html.ButtonType import web.html.HTMLInputElement @@ -122,7 +124,7 @@ class RegistrationView : AbstractView( responseHandler = ::classComponentResponseHandlerWithValidation, ) if (response.ok) { - window.location.href = "#/${FrontendRoutes.PROJECTS.path}" + window.location.href = "#" window.location.reload() } else if (response.isConflict()) { val responseText = response.unpackMessage() @@ -137,6 +139,15 @@ class RegistrationView : AbstractView( "EMPTY_BLOCK_STRUCTURE_ERROR", ) override fun ChildrenBuilder.render() { + particles() + + if (props.userInfo?.isActive != false) { + Navigate { + to = "/" + replace = false + } + } + main { className = ClassName("main-content mt-0 ps") div { @@ -150,6 +161,9 @@ class RegistrationView : AbstractView( className = ClassName("col-sm-4") div { className = ClassName("container card o-hidden border-0 shadow-lg my-2 card-body p-0") + style = jso { + zIndex = (MAX_Z_INDEX - 1).unsafeCast() + } div { className = ClassName("p-5 text-center") renderTitle() @@ -194,7 +208,7 @@ class RegistrationView : AbstractView( src = props.userInfo?.avatar?.let { "/api/$v1/avatar$it" } - ?: "img/undraw_profile.svg" + ?: AVATAR_PROFILE_PLACEHOLDER style = jso { height = 16.rem width = 16.rem diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexLogoButtons.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexLogoButtons.kt index 0557f226f6..b32a33b8ab 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexLogoButtons.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexLogoButtons.kt @@ -67,7 +67,7 @@ private fun ChildrenBuilder.creationCard(url: String, img: String) { (img { src = img style = jso { - width = 20.rem + width = 17.rem border = "0.2rem solid hsl(186 100% 69%)".unsafeCast() } }) diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexViewAuth.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexViewAuth.kt index f4a3989f78..56825084a0 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexViewAuth.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexViewAuth.kt @@ -4,10 +4,8 @@ package com.saveourtool.save.frontend.components.views.index -import com.saveourtool.save.frontend.externals.fontawesome.faCopyright -import com.saveourtool.save.frontend.externals.fontawesome.faGithub -import com.saveourtool.save.frontend.externals.fontawesome.faSignInAlt -import com.saveourtool.save.frontend.externals.fontawesome.fontAwesomeIcon +import com.saveourtool.save.frontend.components.views.welcome.mappingFromTypeToFontLogo +import com.saveourtool.save.frontend.externals.fontawesome.* import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.frontend.utils.noopResponseHandler import com.saveourtool.save.info.OauthProviderInfo @@ -48,24 +46,27 @@ val indexAuth: FC = FC { props -> div { className = ClassName("row mt-2") div { - className = ClassName("col-3 text-center") + className = ClassName("col-4 text-center") } div { - className = ClassName("col-6 text-center") + className = ClassName("col-4 text-center") @Suppress("MAGIC_NUMBER") - oauthProviders.map { - oauthLogin( - 4.rem, it, "animate__backInUp", - when (it.registrationId) { - "github" -> faGithub - "codehub" -> faCopyright - else -> faSignInAlt - } - ) + div { + className = ClassName("row") + oauthProviders.map { userInfo -> + val oauthProvider = userInfo.registrationId + oauthLogin( + 4.rem, + userInfo, + "animate__backInUp", + oauthProvider.replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }, + mappingFromTypeToFontLogo(oauthProvider) + ) + } } } div { - className = ClassName("col-3 text-center") + className = ClassName("col-4 text-center") } } } @@ -96,19 +97,31 @@ val separator = VFC { * @param provider oauth provider (Huawei, Gitee, Github, etc.) * @param icon icon logo * @param animate + * @param label */ fun ChildrenBuilder.oauthLogin( size: FontSize, provider: OauthProviderInfo, animate: String, + label: String = "", icon: dynamic ) { - a { - href = provider.authorizationLink - className = ClassName("btn btn-link px-5 text-white text-lg text-center animate__animated $animate") - style = jso { - fontSize = size + div { + className = ClassName("animated-provider col animate__animated $animate") + a { + href = provider.authorizationLink + className = ClassName("text-center") + div { + className = ClassName("col text-center text-white") + style = jso { + fontSize = size + } + fontAwesomeIcon(icon = icon) + } + div { + className = ClassName("col text-center text-white") + +label + } } - fontAwesomeIcon(icon = icon) } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/toprating/UserRatingTab.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/toprating/UserRatingTab.kt index 5fb2479960..fd76dfe91e 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/toprating/UserRatingTab.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/toprating/UserRatingTab.kt @@ -70,7 +70,7 @@ val userRatingTable: FC = FC { _ -> ClassName("avatar avatar-user width-full border color-bg-default rounded-circle") src = cellContext.row.original.avatar?.let { "/api/$v1/avatar$it" - } ?: "img/undraw_profile.svg" + } ?: AVATAR_PROFILE_PLACEHOLDER style = jso { height = 2.rem width = 2.rem diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileView.kt index 27b3ac98e5..4284aff714 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileView.kt @@ -148,7 +148,7 @@ fun ChildrenBuilder.renderLeftUserMenu( "/api/$v1/avatar$path" } ?: run { - "img/undraw_profile.svg" + AVATAR_PROFILE_PLACEHOLDER } alt = "" } @@ -272,7 +272,7 @@ fun ChildrenBuilder.renderLeftUserMenu( "/api/$v1/avatar$path" } ?: run { - "img/undraw_profile.svg" + AVATAR_PROFILE_PLACEHOLDER } alt = "" } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/UserSettingsView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/UserSettingsView.kt index 5c062753f8..79ce6eba0e 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/UserSettingsView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/UserSettingsView.kt @@ -120,7 +120,7 @@ abstract class UserSettingsView : AbstractView(true) { backgroundColor = "#3075c0".unsafeCast() } h4 { - className = ClassName("text-white font-weight-bolder text-center mt-2 mb-0") + className = ClassName("text-white font-weight-bolder text-center mt-2 mb-3") +"Sign in" } div { className = ClassName("row") - div { - className = ClassName("col text-center ") - state.oauthProviders?.map { - oauthLogin( - 3.4.rem, it, "", - when (it.registrationId) { - "github" -> faGithub - "codehub" -> faCopyright - else -> faSignInAlt - } - ) - } + state.oauthProviders?.map { + oauthLogin( + 3.4.rem, + it, + "", + "", + mappingFromTypeToFontLogo(it.registrationId) + ) } } } @@ -292,3 +288,14 @@ class WelcomeView : AbstractView(true) { } } } + +/** + * @param registrationId oauth provider name from api-gateway + */ +fun mappingFromTypeToFontLogo(registrationId: String) = + when (registrationId) { + "github" -> faGithub + "codehub" -> faCopyright + "gitee" -> faSignInAlt + else -> faSignInAlt + } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/Utils.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/Utils.kt index f63293ff6b..62afd37476 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/Utils.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/Utils.kt @@ -37,6 +37,11 @@ import kotlinx.serialization.json.Json */ internal const val AVATAR_PLACEHOLDER = "img/undraw_image_not_found.png" +/** + * Avatar profile for those who don't want to upload it + */ +internal const val AVATAR_PROFILE_PLACEHOLDER = "img/undraw_profile.svg" + /** * The body of a [useDeferredRequest] invocation. * diff --git a/save-frontend/src/main/resources/scss/_glow.scss b/save-frontend/src/main/resources/scss/_glow.scss index 338178ccc9..70a0452735 100644 --- a/save-frontend/src/main/resources/scss/_glow.scss +++ b/save-frontend/src/main/resources/scss/_glow.scss @@ -177,28 +177,6 @@ } } -@keyframes border-flicker { - 0% { - opacity: 0.1; - } - 2% { - opacity: 1; - } - 4% { - opacity: 0.1; - } - - 8% { - opacity: 1; - } - 70% { - opacity: 0.7; - } - 100% { - opacity: 1; - } -} - @media only screen and (max-width: 600px) { .glowing-btn{ font-size: 1em; @@ -231,3 +209,13 @@ a:hover { opacity: 1; transition: 0.7s } + +// ======= oauth providers ====== +.animated-provider .col { + transition: 0.4s +} + +.animated-provider:hover .col { + transition: 0.4s; + transform: scale(1.2); +} diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt index 71888d7df8..e117254fc5 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt @@ -35,8 +35,9 @@ class TopBarTest { val userInfoSpan: HTMLSpanElement? = screen.queryByTextAndCast("Test User") assertNotNull(userInfoSpan) - // push the button - val button = screen.getByRole("button", jso { name = "Test User" }) + // push the button, this weird test searches for the button that contains a name "Test User" + // and "User setting" hint label + val button = screen.getByRole("button", jso { name = "Test UserUser settings" }) userEvent.click(button) val dropdown = rr.container.querySelector("[aria-labelledby=\"userDropdown\"]") as HTMLDivElement assertEquals(4, dropdown.children.length, "When user is logged in, dropdown menu should contain 3 entries")