Added status controllers, and start of user settings page.

This commit is contained in:
Andrew Lalis 2023-02-16 08:11:48 +01:00
parent aa843227d2
commit 0564cd5789
13 changed files with 174 additions and 30 deletions

View File

@ -6,23 +6,37 @@ const api = axios.create({
});
/**
* Searches for gyms using the given query, and eventually returns results.
* @param query The query to use.
* The search API class.
*/
export async function searchGyms(
query: string
): Promise<Array<GymSearchResult>> {
const response = await api.get('/search/gyms?q=' + query);
return response.data;
class SearchApi {
/**
* Searches for gyms using the given query, and eventually returns results.
* @param query The query to use.
*/
public async searchGyms(query: string): Promise<Array<GymSearchResult>> {
const response = await api.get(`/search/gyms?q=${query}`);
return response.data;
}
/**
* Searches for users using the given query, and eventually returns results.
* Note that only users whose accounts are not private will be included in
* search results.
* @param query The query to use.
*/
public async searchUsers(query: string): Promise<Array<UserSearchResult>> {
const response = await api.get(`/search/users?q=${query}`);
return response.data;
}
public async getStatus(): Promise<boolean> {
try {
const response = await api.get(`/status`);
return true;
} catch (error) {
return false;
}
}
}
/**
* Searches for users using the given query, and eventually returns results.
* Note that only users whose accounts are not private will be included in
* search results.
* @param query The query to use.
*/
export async function searchUsers(query: string): Promise<Array<UserSearchResult>> {
const response = await api.get('/search/users?q=' + query);
return response.data;
}
export default new SearchApi();

View File

@ -3,14 +3,19 @@
<q-btn-dropdown
color="primary"
:label="authStore.user?.name"
v-if="authStore.loggedIn"
v-if="authStore.loggedIn && authStore.user"
no-caps
icon="person"
>
<q-list>
<q-item clickable v-close-popup :to="'/users/' + authStore.user?.id">
<q-item clickable v-close-popup :to="getUserRoute(authStore.user)">
<q-item-section>
<q-item-label>{{ $t('accountMenuItem.myAccount') }}</q-item-label>
<q-item-label>{{ $t('accountMenuItem.profile') }}</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup :to="getUserRoute(authStore.user) + '/settings'">
<q-item-section>
<q-item-label>{{ $t('accountMenuItem.settings') }}</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="api.auth.logout(authStore)">
@ -35,6 +40,7 @@
import { useAuthStore } from 'stores/auth-store';
import api from 'src/api/main';
import { useRoute, useRouter } from 'vue-router';
import { getUserRoute } from 'src/router/user-routing';
const authStore = useAuthStore();
const route = useRoute();

View File

@ -17,6 +17,7 @@ export default {
password: 'Password',
logIn: 'Log in',
createAccount: 'Create an account',
authFailed: 'Invalid credentials.'
},
indexPage: {
searchHint: 'Search for a Gym',
@ -45,9 +46,16 @@ export default {
description: 'We couldn\'t find the user you\'re looking for.'
}
},
userSettingsPage: {
title: 'Account Settings'
},
accountMenuItem: {
logIn: 'Login',
myAccount: 'My Account',
profile: 'Profile',
settings: 'Settings',
logOut: 'Log out',
},
generalErrors: {
apiError: 'An API error occurred. Please try again later.'
}
};

View File

@ -7,3 +7,23 @@ export default {
'nl-NL': nlNL,
de: de,
};
export const supportedLocales = [
{ value: 'en-US', label: 'English' },
{ value: 'nl-NL', label: 'Nederlands' },
{ value: 'de', label: 'Deutsch' },
];
/**
* Tries to find a locale with the given code, or defaults to the base locale.
* @param code The locale code.
* @returns The locale that was resolved.
*/
export function resolveLocale(code?: string) {
for (const loc of supportedLocales) {
if (loc.value === code) {
return loc;
}
}
return supportedLocales[0];
}

View File

@ -17,7 +17,6 @@
>
</q-toolbar-title>
<AccountMenuItem />
<LocaleSelect />
</q-toolbar>
</q-header>
@ -41,7 +40,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import LocaleSelect from 'components/LocaleSelect.vue';
import AccountMenuItem from 'components/AccountMenuItem.vue';
const leftDrawerOpen = ref(false);

View File

@ -30,7 +30,8 @@ import { useRoute, useRouter } from 'vue-router';
import GymSearchResultListItem from 'components/GymSearchResultListItem.vue';
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
import { GymSearchResult } from 'src/api/search/models';
import { searchGyms } from 'src/api/search';
import searchApi from 'src/api/search';
import { sleep } from 'src/utils';
const route = useRoute();
const router = useRouter();
@ -47,6 +48,7 @@ onMounted(async () => {
if (route.query.search_query) {
searchQuery.value = route.query.search_query as string;
searchBarLoadingState.value = true;
await sleep(500);
await doSearch();
}
});
@ -74,7 +76,7 @@ async function doSearch() {
}
await router.push({ path: '/', query: query });
try {
searchResults.value = await searchGyms(searchQueryText);
searchResults.value = await searchApi.searchGyms(searchQueryText);
} catch (error) {
console.error(error);
} finally {

View File

@ -2,8 +2,13 @@
<q-page>
<StandardCenteredPage v-if="user">
<h3>{{ user?.name }}</h3>
<p>{{ user?.email }}</p>
<p v-if="isOwnUser">This is your account!</p>
<hr>
</StandardCenteredPage>
<StandardCenteredPage v-if="userNotFound">
<h3>{{ $t('userPage.notFound.title') }}</h3>
@ -14,11 +19,12 @@
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import {onMounted, ref, Ref} from 'vue';
import {computed, onMounted, ref, Ref} from 'vue';
import {User} from 'src/api/main/auth';
import api from 'src/api/main';
import {useRoute} from 'vue-router';
import {useAuthStore} from 'stores/auth-store';
import { getUserRoute } from 'src/router/user-routing';
const route = useRoute();
const authStore = useAuthStore();
@ -48,7 +54,7 @@ onMounted(async () => {
userNotFound.value = true;
}
}
isOwnUser.value = authStore.loggedIn && user.value.id === authStore.user?.id;
isOwnUser.value = authStore.loggedIn && user.value?.id === authStore.user?.id;
});
</script>

View File

@ -59,10 +59,15 @@ import { ref } from 'vue';
import api from 'src/api/main';
import { useAuthStore } from 'stores/auth-store';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { resolveLocale } from 'src/i18n';
import { useQuasar } from 'quasar';
const authStore = useAuthStore();
const router = useRouter();
const route = useRoute();
const i18n = useI18n({ useScope: 'global' });
const quasar = useQuasar();
const loginModel = ref({
email: '',
@ -70,15 +75,40 @@ const loginModel = ref({
});
const passwordVisible = ref(false);
/**
* The main login function. It attempts to log in the user, and gracefully
* handles failures.
*
* Upon successful login, we set the app's locale to the user's preferred
* locale, and then redirect to the pre-configured next URL (if there is one).
*/
async function tryLogin() {
try {
await api.auth.login(authStore, loginModel.value);
// Set the locale to the user's preferred locale.
i18n.locale.value = resolveLocale(authStore.user?.preferences?.locale);
// Redirect back to whatever was set as the next URL.
const dest = route.query.next
? decodeURIComponent(route.query.next as string)
: '/';
await router.push(dest);
} catch (error) {
console.error(error);
} catch (error: any) {
if (error.response && error.response.status === 401) {
quasar.notify({
message: i18n.t('loginPage.authFailed'),
type: 'warning',
position: 'top',
});
} else {
quasar.notify({
message: i18n.t('generalErrors.apiError'),
type: 'negative',
position: 'top',
});
console.error(error);
}
}
}

View File

@ -0,0 +1,14 @@
<template>
<q-page>
<StandardCenteredPage>
<h3>{{ $t('userSettingsPage.title') }}</h3>
</StandardCenteredPage>
</q-page>
</template>
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
</script>
<style scoped>
</style>

View File

@ -13,6 +13,7 @@ import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
import ActivationPage from 'pages/auth/ActivationPage.vue';
import SubmissionPage from 'pages/SubmissionPage.vue';
import UserPage from 'pages/UserPage.vue';
import UserSettingsPage from 'pages/auth/UserSettingsPage.vue';
const routes: RouteRecordRaw[] = [
// Auth-related pages, which live outside the main layout.
@ -28,7 +29,15 @@ const routes: RouteRecordRaw[] = [
children: [
{ path: '', component: IndexPage },
{ path: 'testing', component: TestingPage },
{ path: 'users/:userId', component: UserPage },
{
path: 'users/:userId',
children: [
{ path: '', component: UserPage },
{ path: 'settings', component: UserSettingsPage }
]
},
// { path: 'users/:userId', component: UserPage },
// { path: 'users/:userId/settings', component: UserSettingsPage },
{
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
component: GymPage,

View File

@ -0,0 +1,5 @@
import { User } from 'src/api/main/auth';
export function getUserRoute(user: User) {
return `/users/${user.id}`;
}

View File

@ -0,0 +1,17 @@
package nl.andrewlalis.gymboardcdn.api;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping
public class StatusController {
@GetMapping(path = "/status")
public ResponseEntity<?> getStatus() {
return ResponseEntity.ok(Map.of("online", true));
}
}

View File

@ -0,0 +1,15 @@
package nl.andrewlalis.gymboardsearch;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class StatusController {
@GetMapping(path = "/status")
public ResponseEntity<?> getStatus() {
return ResponseEntity.ok(Map.of("online", true));
}
}