Added status controllers, and start of user settings page.
This commit is contained in:
parent
aa843227d2
commit
0564cd5789
|
@ -5,14 +5,16 @@ const api = axios.create({
|
||||||
baseURL: 'http://localhost:8081',
|
baseURL: 'http://localhost:8081',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search API class.
|
||||||
|
*/
|
||||||
|
class SearchApi {
|
||||||
/**
|
/**
|
||||||
* Searches for gyms using the given query, and eventually returns results.
|
* Searches for gyms using the given query, and eventually returns results.
|
||||||
* @param query The query to use.
|
* @param query The query to use.
|
||||||
*/
|
*/
|
||||||
export async function searchGyms(
|
public async searchGyms(query: string): Promise<Array<GymSearchResult>> {
|
||||||
query: string
|
const response = await api.get(`/search/gyms?q=${query}`);
|
||||||
): Promise<Array<GymSearchResult>> {
|
|
||||||
const response = await api.get('/search/gyms?q=' + query);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +24,19 @@ export async function searchGyms(
|
||||||
* search results.
|
* search results.
|
||||||
* @param query The query to use.
|
* @param query The query to use.
|
||||||
*/
|
*/
|
||||||
export async function searchUsers(query: string): Promise<Array<UserSearchResult>> {
|
public async searchUsers(query: string): Promise<Array<UserSearchResult>> {
|
||||||
const response = await api.get('/search/users?q=' + query);
|
const response = await api.get(`/search/users?q=${query}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getStatus(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/status`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SearchApi();
|
||||||
|
|
|
@ -3,14 +3,19 @@
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="authStore.user?.name"
|
:label="authStore.user?.name"
|
||||||
v-if="authStore.loggedIn"
|
v-if="authStore.loggedIn && authStore.user"
|
||||||
no-caps
|
no-caps
|
||||||
icon="person"
|
icon="person"
|
||||||
>
|
>
|
||||||
<q-list>
|
<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-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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable v-close-popup @click="api.auth.logout(authStore)">
|
<q-item clickable v-close-popup @click="api.auth.logout(authStore)">
|
||||||
|
@ -35,6 +40,7 @@
|
||||||
import { useAuthStore } from 'stores/auth-store';
|
import { useAuthStore } from 'stores/auth-store';
|
||||||
import api from 'src/api/main';
|
import api from 'src/api/main';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { getUserRoute } from 'src/router/user-routing';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default {
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
logIn: 'Log in',
|
logIn: 'Log in',
|
||||||
createAccount: 'Create an account',
|
createAccount: 'Create an account',
|
||||||
|
authFailed: 'Invalid credentials.'
|
||||||
},
|
},
|
||||||
indexPage: {
|
indexPage: {
|
||||||
searchHint: 'Search for a Gym',
|
searchHint: 'Search for a Gym',
|
||||||
|
@ -45,9 +46,16 @@ export default {
|
||||||
description: 'We couldn\'t find the user you\'re looking for.'
|
description: 'We couldn\'t find the user you\'re looking for.'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
userSettingsPage: {
|
||||||
|
title: 'Account Settings'
|
||||||
|
},
|
||||||
accountMenuItem: {
|
accountMenuItem: {
|
||||||
logIn: 'Login',
|
logIn: 'Login',
|
||||||
myAccount: 'My Account',
|
profile: 'Profile',
|
||||||
|
settings: 'Settings',
|
||||||
logOut: 'Log out',
|
logOut: 'Log out',
|
||||||
},
|
},
|
||||||
|
generalErrors: {
|
||||||
|
apiError: 'An API error occurred. Please try again later.'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,3 +7,23 @@ export default {
|
||||||
'nl-NL': nlNL,
|
'nl-NL': nlNL,
|
||||||
de: de,
|
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];
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
>
|
>
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
<AccountMenuItem />
|
<AccountMenuItem />
|
||||||
<LocaleSelect />
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
|
@ -41,7 +40,6 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import LocaleSelect from 'components/LocaleSelect.vue';
|
|
||||||
import AccountMenuItem from 'components/AccountMenuItem.vue';
|
import AccountMenuItem from 'components/AccountMenuItem.vue';
|
||||||
|
|
||||||
const leftDrawerOpen = ref(false);
|
const leftDrawerOpen = ref(false);
|
||||||
|
|
|
@ -30,7 +30,8 @@ import { useRoute, useRouter } from 'vue-router';
|
||||||
import GymSearchResultListItem from 'components/GymSearchResultListItem.vue';
|
import GymSearchResultListItem from 'components/GymSearchResultListItem.vue';
|
||||||
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
|
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
|
||||||
import { GymSearchResult } from 'src/api/search/models';
|
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 route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -47,6 +48,7 @@ onMounted(async () => {
|
||||||
if (route.query.search_query) {
|
if (route.query.search_query) {
|
||||||
searchQuery.value = route.query.search_query as string;
|
searchQuery.value = route.query.search_query as string;
|
||||||
searchBarLoadingState.value = true;
|
searchBarLoadingState.value = true;
|
||||||
|
await sleep(500);
|
||||||
await doSearch();
|
await doSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -74,7 +76,7 @@ async function doSearch() {
|
||||||
}
|
}
|
||||||
await router.push({ path: '/', query: query });
|
await router.push({ path: '/', query: query });
|
||||||
try {
|
try {
|
||||||
searchResults.value = await searchGyms(searchQueryText);
|
searchResults.value = await searchApi.searchGyms(searchQueryText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
<q-page>
|
<q-page>
|
||||||
<StandardCenteredPage v-if="user">
|
<StandardCenteredPage v-if="user">
|
||||||
<h3>{{ user?.name }}</h3>
|
<h3>{{ user?.name }}</h3>
|
||||||
|
|
||||||
<p>{{ user?.email }}</p>
|
<p>{{ user?.email }}</p>
|
||||||
<p v-if="isOwnUser">This is your account!</p>
|
<p v-if="isOwnUser">This is your account!</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
|
||||||
</StandardCenteredPage>
|
</StandardCenteredPage>
|
||||||
<StandardCenteredPage v-if="userNotFound">
|
<StandardCenteredPage v-if="userNotFound">
|
||||||
<h3>{{ $t('userPage.notFound.title') }}</h3>
|
<h3>{{ $t('userPage.notFound.title') }}</h3>
|
||||||
|
@ -14,11 +19,12 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
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 {User} from 'src/api/main/auth';
|
||||||
import api from 'src/api/main';
|
import api from 'src/api/main';
|
||||||
import {useRoute} from 'vue-router';
|
import {useRoute} from 'vue-router';
|
||||||
import {useAuthStore} from 'stores/auth-store';
|
import {useAuthStore} from 'stores/auth-store';
|
||||||
|
import { getUserRoute } from 'src/router/user-routing';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
@ -48,7 +54,7 @@ onMounted(async () => {
|
||||||
userNotFound.value = true;
|
userNotFound.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isOwnUser.value = authStore.loggedIn && user.value.id === authStore.user?.id;
|
isOwnUser.value = authStore.loggedIn && user.value?.id === authStore.user?.id;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -59,10 +59,15 @@ import { ref } from 'vue';
|
||||||
import api from 'src/api/main';
|
import api from 'src/api/main';
|
||||||
import { useAuthStore } from 'stores/auth-store';
|
import { useAuthStore } from 'stores/auth-store';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { resolveLocale } from 'src/i18n';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const i18n = useI18n({ useScope: 'global' });
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
const loginModel = ref({
|
const loginModel = ref({
|
||||||
email: '',
|
email: '',
|
||||||
|
@ -70,17 +75,42 @@ const loginModel = ref({
|
||||||
});
|
});
|
||||||
const passwordVisible = ref(false);
|
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() {
|
async function tryLogin() {
|
||||||
try {
|
try {
|
||||||
await api.auth.login(authStore, loginModel.value);
|
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
|
const dest = route.query.next
|
||||||
? decodeURIComponent(route.query.next as string)
|
? decodeURIComponent(route.query.next as string)
|
||||||
: '/';
|
: '/';
|
||||||
await router.push(dest);
|
await router.push(dest);
|
||||||
} catch (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);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetLogin() {
|
function resetLogin() {
|
||||||
loginModel.value.email = '';
|
loginModel.value.email = '';
|
||||||
|
|
|
@ -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>
|
|
@ -13,6 +13,7 @@ import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
|
||||||
import ActivationPage from 'pages/auth/ActivationPage.vue';
|
import ActivationPage from 'pages/auth/ActivationPage.vue';
|
||||||
import SubmissionPage from 'pages/SubmissionPage.vue';
|
import SubmissionPage from 'pages/SubmissionPage.vue';
|
||||||
import UserPage from 'pages/UserPage.vue';
|
import UserPage from 'pages/UserPage.vue';
|
||||||
|
import UserSettingsPage from 'pages/auth/UserSettingsPage.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
// Auth-related pages, which live outside the main layout.
|
// Auth-related pages, which live outside the main layout.
|
||||||
|
@ -28,7 +29,15 @@ const routes: RouteRecordRaw[] = [
|
||||||
children: [
|
children: [
|
||||||
{ path: '', component: IndexPage },
|
{ path: '', component: IndexPage },
|
||||||
{ path: 'testing', component: TestingPage },
|
{ 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',
|
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
|
||||||
component: GymPage,
|
component: GymPage,
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { User } from 'src/api/main/auth';
|
||||||
|
|
||||||
|
export function getUserRoute(user: User) {
|
||||||
|
return `/users/${user.id}`;
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue