Added lots of stuff!!!
This commit is contained in:
parent
97c69ea34d
commit
ae3c22422c
|
@ -49,6 +49,7 @@ public class SecurityConfig {
|
|||
"/submissions/**",
|
||||
"/auth/reset-password",
|
||||
"/auth/users/*",
|
||||
"/auth/users/*/access",
|
||||
"/auth/users/*/followers",
|
||||
"/auth/users/*/following",
|
||||
"/users/*/recent-submissions"
|
||||
|
|
|
@ -2,10 +2,10 @@ package nl.andrewlalis.gymboard_api.domains.api.controller;
|
|||
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/submissions")
|
||||
|
@ -20,4 +20,10 @@ public class SubmissionController {
|
|||
public SubmissionResponse getSubmission(@PathVariable String submissionId) {
|
||||
return submissionService.getSubmission(submissionId);
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/{submissionId}")
|
||||
public ResponseEntity<Void> deleteSubmission(@PathVariable String submissionId, @AuthenticationPrincipal User user) {
|
||||
submissionService.deleteSubmission(submissionId, user);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,4 +132,15 @@ public class ExerciseSubmissionService {
|
|||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteSubmission(String submissionId, User user) {
|
||||
Submission submission = submissionRepository.findById(submissionId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!submission.getUser().getId().equals(user.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete other user's submission.");
|
||||
}
|
||||
// TODO: Find a secure way to delete the associated video.
|
||||
submissionRepository.delete(submission);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package nl.andrewlalis.gymboard_api.domains.auth.controller;
|
|||
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.*;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserAccessService;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
@ -12,9 +13,11 @@ import org.springframework.web.bind.annotation.*;
|
|||
@RestController
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
private final UserAccessService userAccessService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
public UserController(UserService userService, UserAccessService userAccessService) {
|
||||
this.userService = userService;
|
||||
this.userAccessService = userAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,6 +36,11 @@ public class UserController {
|
|||
return userService.getUser(userId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/users/{userId}/access")
|
||||
public UserAccessResponse getUserAccess(@PathVariable String userId) {
|
||||
return new UserAccessResponse(userAccessService.currentUserHasAccess(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for updating one's own password.
|
||||
* @param user The user that's updating their password.
|
||||
|
@ -71,6 +79,11 @@ public class UserController {
|
|||
return userService.updatePreferences(user.getId(), payload);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/users/{user1Id}/relationship-to/{user2Id}")
|
||||
public UserRelationshipResponse getRelationship(@PathVariable String user1Id, @PathVariable String user2Id) {
|
||||
return userService.getRelationship(user1Id, user2Id);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/auth/users/{userId}/followers")
|
||||
public ResponseEntity<Void> followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
|
||||
userService.followUser(myUser.getId(), userId);
|
||||
|
@ -92,4 +105,14 @@ public class UserController {
|
|||
public Page<UserResponse> getFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
|
||||
return userService.getFollowing(user.getId(), pageable);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/users/{userId}/followers")
|
||||
public Page<UserResponse> getUserFollowers(@PathVariable String userId, Pageable pageable) {
|
||||
return userService.getFollowers(userId, pageable);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/users/{userId}/following")
|
||||
public Page<UserResponse> getUserFollowing(@PathVariable String userId, Pageable pageable) {
|
||||
return userService.getFollowing(userId, pageable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record UserAccessResponse(boolean accessible) {}
|
|
@ -0,0 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record UserRelationshipResponse(
|
||||
boolean following,
|
||||
boolean followedBy
|
||||
) {}
|
|
@ -36,6 +36,9 @@ public class UserAccessService {
|
|||
if (targetUser != null && !targetUser.getPreferences().isAccountPrivate()) {
|
||||
return true;
|
||||
}
|
||||
if (user != null && targetUser != null && user.getId().equals(targetUser.getId())) {
|
||||
return true;
|
||||
}
|
||||
return user != null && followingRepository.existsByFollowedUserAndFollowingUser(targetUser, user);
|
||||
}
|
||||
|
||||
|
@ -79,9 +82,10 @@ public class UserAccessService {
|
|||
@Transactional(readOnly = true)
|
||||
public boolean currentUserHasAccess(String userId) {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
User user = null;
|
||||
if (auth instanceof TokenAuthentication tokenAuth) {
|
||||
return userHasAccess(tokenAuth.user(), userId);
|
||||
user = tokenAuth.user();
|
||||
}
|
||||
return false;
|
||||
return userHasAccess(user, userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public class UserService {
|
|||
private final UserActivationCodeRepository activationCodeRepository;
|
||||
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
||||
private final UserFollowingRepository userFollowingRepository;
|
||||
private final UserAccessService userAccessService;
|
||||
private final ULID ulid;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
|
@ -51,7 +52,7 @@ public class UserService {
|
|||
UserPreferencesRepository userPreferencesRepository,
|
||||
UserActivationCodeRepository activationCodeRepository,
|
||||
PasswordResetCodeRepository passwordResetCodeRepository,
|
||||
UserFollowingRepository userFollowingRepository, ULID ulid,
|
||||
UserFollowingRepository userFollowingRepository, UserAccessService userAccessService, ULID ulid,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender
|
||||
) {
|
||||
|
@ -61,6 +62,7 @@ public class UserService {
|
|||
this.activationCodeRepository = activationCodeRepository;
|
||||
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
||||
this.userFollowingRepository = userFollowingRepository;
|
||||
this.userAccessService = userAccessService;
|
||||
this.ulid = ulid;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
|
@ -280,6 +282,7 @@ public class UserService {
|
|||
|
||||
@Transactional
|
||||
public void followUser(String followerId, String followedId) {
|
||||
if (followerId.equals(followedId)) return;
|
||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
|
||||
|
@ -290,6 +293,7 @@ public class UserService {
|
|||
|
||||
@Transactional
|
||||
public void unfollowUser(String followerId, String followedId) {
|
||||
if (followerId.equals(followedId)) return;
|
||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
|
||||
|
@ -300,6 +304,7 @@ public class UserService {
|
|||
public Page<UserResponse> getFollowers(String userId, Pageable pageable) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
userAccessService.enforceUserAccess(user);
|
||||
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
|
||||
.map(UserFollowing::getFollowingUser)
|
||||
.map(UserResponse::new);
|
||||
|
@ -309,8 +314,24 @@ public class UserService {
|
|||
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
userAccessService.enforceUserAccess(user);
|
||||
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
|
||||
.map(UserFollowing::getFollowedUser)
|
||||
.map(UserResponse::new);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserRelationshipResponse getRelationship(String user1Id, String user2Id) {
|
||||
User user1 = userRepository.findById(user1Id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
User user2 = userRepository.findById(user2Id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
userAccessService.enforceUserAccess(user1);
|
||||
boolean user1FollowingUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user2, user1);
|
||||
boolean user1FollowedByUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user1, user2);
|
||||
return new UserRelationshipResponse(
|
||||
user1FollowingUser2,
|
||||
user1FollowedByUser2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,3 +17,5 @@ spring.mail.properties.mail.smtp.timeout=10000
|
|||
app.auth.private-key-location=./private_key.der
|
||||
app.web-origin=http://localhost:9000
|
||||
app.cdn-origin=http://localhost:8082
|
||||
|
||||
#logging.level.root=DEBUG
|
||||
|
|
|
@ -27,6 +27,11 @@ export interface UserPersonalDetails {
|
|||
sex: PersonSex;
|
||||
}
|
||||
|
||||
export interface UserRelationship {
|
||||
following: boolean;
|
||||
followedBy: boolean;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
userId: string;
|
||||
accountPrivate: boolean;
|
||||
|
@ -104,6 +109,11 @@ class AuthModule {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
public async isUserAccessible(userId: string, authStore: AuthStoreType): Promise<boolean> {
|
||||
const response = await api.get(`/auth/users/${userId}/access`, authStore.axiosConfig);
|
||||
return response.data.accessible;
|
||||
}
|
||||
|
||||
public async updatePassword(newPassword: string, authStore: AuthStoreType) {
|
||||
await api.post(
|
||||
'/auth/me/password',
|
||||
|
@ -142,6 +152,29 @@ class AuthModule {
|
|||
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getFollowers(userId: string, authStore: AuthStoreType, page: number, count: number): Promise<User[]> {
|
||||
const response = await api.get(`/auth/users/${userId}/followers?page=${page}&count=${count}`, authStore.axiosConfig);
|
||||
return response.data.content;
|
||||
}
|
||||
|
||||
public async getFollowing(userId: string, authStore: AuthStoreType, page: number, count: number): Promise<User[]> {
|
||||
const response = await api.get(`/auth/users/${userId}/following?page=${page}&count=${count}`, authStore.axiosConfig);
|
||||
return response.data.content;
|
||||
}
|
||||
|
||||
public async getRelationshipTo(userId: string, targetUserId: string, authStore: AuthStoreType): Promise<UserRelationship> {
|
||||
const response = await api.get(`/auth/users/${userId}/relationship-to/${targetUserId}`, authStore.axiosConfig);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async followUser(userId: string, authStore: AuthStoreType) {
|
||||
await api.post(`/auth/users/${userId}/followers`, undefined, authStore.axiosConfig);
|
||||
}
|
||||
|
||||
public async unfollowUser(userId: string, authStore: AuthStoreType) {
|
||||
await api.delete(`/auth/users/${userId}/followers`, authStore.axiosConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthModule;
|
||||
|
|
|
@ -66,6 +66,10 @@ class SubmissionsModule {
|
|||
const response = await api.post(`/gyms/${gymId}/submissions`, payload, authStore.axiosConfig);
|
||||
return parseSubmission(response.data);
|
||||
}
|
||||
|
||||
public async deleteSubmission(id: string, authStore: AuthStoreType) {
|
||||
await api.delete(`/submissions/${id}`, authStore.axiosConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubmissionsModule;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<!--
|
||||
A menu for a page, in which items refer to different sub-pages.
|
||||
-->
|
||||
<template>
|
||||
<q-btn-group spread square push>
|
||||
<q-btn
|
||||
v-for="(item, index) in items" :key="index"
|
||||
:label="item.label"
|
||||
:to="getItemPath(item)"
|
||||
:color="isItemSelected(index) ? 'primary' : 'secondary'"
|
||||
/>
|
||||
</q-btn-group>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useRoute} from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
to?: string;
|
||||
}
|
||||
interface Props {
|
||||
items: MenuItem[];
|
||||
baseRoute: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
function isItemSelected(index: number): boolean {
|
||||
const item = props.items[index];
|
||||
return route.path === getItemPath(item);
|
||||
}
|
||||
|
||||
function getItemPath(item: MenuItem): string {
|
||||
if (!item.to || item.to === '') return props.baseRoute;
|
||||
return props.baseRoute + '/' + item.to;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -3,7 +3,7 @@
|
|||
<q-item-section>
|
||||
<q-item-label>{{ user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side top>
|
||||
<q-item-section side top v-if="user.submissionCount">
|
||||
<q-badge color="primary" :label="submissionCountLabel"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
|
|
@ -20,6 +20,13 @@
|
|||
<p>
|
||||
{{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
|
||||
</p>
|
||||
|
||||
<!-- Deletion button is only visible if the user who submitted it is viewing it. -->
|
||||
<q-btn
|
||||
v-if="authStore.user && authStore.user.id === submission.user.id"
|
||||
label="Delete"
|
||||
@click="deleteSubmission"
|
||||
/>
|
||||
</StandardCenteredPage>
|
||||
</q-page>
|
||||
</template>
|
||||
|
@ -33,11 +40,18 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { DateTime } from 'luxon';
|
||||
import { getFileUrl } from 'src/api/cdn';
|
||||
import { getGymRoute } from 'src/router/gym-routing';
|
||||
import {useAuthStore} from "stores/auth-store";
|
||||
import {showApiErrorToast} from "src/utils";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useQuasar} from "quasar";
|
||||
|
||||
const submission: Ref<ExerciseSubmission | undefined> = ref();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const i18n = useI18n();
|
||||
const quasar = useQuasar();
|
||||
|
||||
onMounted(async () => {
|
||||
const submissionId = route.params.submissionId as string;
|
||||
|
@ -48,6 +62,18 @@ onMounted(async () => {
|
|||
await router.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteSubmission() {
|
||||
// TODO: Confirm via a dialog or something before deleting.
|
||||
if (!submission.value) return;
|
||||
try {
|
||||
await api.gyms.submissions.deleteSubmission(submission.value.id, authStore);
|
||||
await router.push('/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.submission-video {
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<q-page>
|
||||
<StandardCenteredPage v-if="user">
|
||||
<h3>{{ user?.name }}</h3>
|
||||
|
||||
<hr>
|
||||
|
||||
<div v-if="userPrivate">
|
||||
This account is private.
|
||||
</div>
|
||||
|
||||
<div v-if="recentSubmissions.length > 0">
|
||||
<h4 class="text-center">{{ $t('userPage.recentLifts') }}</h4>
|
||||
<q-list separator>
|
||||
<ExerciseSubmissionListItem
|
||||
v-for="sub in recentSubmissions"
|
||||
:submission="sub"
|
||||
:key="sub.id"
|
||||
:show-name="false"
|
||||
/>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
</StandardCenteredPage>
|
||||
<StandardCenteredPage v-if="userNotFound">
|
||||
<h3>{{ $t('userPage.notFound.title') }}</h3>
|
||||
<p>{{ $t('userPage.notFound.description') }}</p>
|
||||
</StandardCenteredPage>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import {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 {useI18n} from 'vue-i18n';
|
||||
import {useQuasar} from 'quasar';
|
||||
import {showApiErrorToast} from 'src/utils';
|
||||
import {ExerciseSubmission} from 'src/api/main/submission';
|
||||
import ExerciseSubmissionListItem from 'components/ExerciseSubmissionListItem.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const i18n = useI18n();
|
||||
const quasar = useQuasar();
|
||||
|
||||
/**
|
||||
* The user that this page displays information about.
|
||||
*/
|
||||
const user: Ref<User | undefined> = ref();
|
||||
const recentSubmissions: Ref<ExerciseSubmission[]> = ref([]);
|
||||
const isOwnUser = ref(false);
|
||||
const userNotFound = ref(false);
|
||||
const userPrivate = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const userId = route.params.userId as string;
|
||||
try {
|
||||
user.value = await api.auth.getUser(userId, authStore);
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 404) {
|
||||
userNotFound.value = true;
|
||||
} else {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
isOwnUser.value = authStore.loggedIn && user.value?.id === authStore.user?.id;
|
||||
|
||||
// If the user exists, try and fetch their latest submissions, and handle a 403 forbidden as private.
|
||||
if (user.value) {
|
||||
try {
|
||||
recentSubmissions.value = await api.users.getRecentSubmissions(user.value?.id, authStore);
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
userPrivate.value = true;
|
||||
} else {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -2,23 +2,14 @@
|
|||
<q-page>
|
||||
<StandardCenteredPage v-if="gym">
|
||||
<h3 class="q-my-md text-center">{{ gym.displayName }}</h3>
|
||||
<q-btn-group spread square push>
|
||||
<q-btn
|
||||
:label="$t('gymPage.home')"
|
||||
:to="getGymRoute(gym)"
|
||||
:color="homePageSelected ? 'primary' : 'secondary'"
|
||||
<PageMenu
|
||||
:base-route="getGymRoute(gym)"
|
||||
:items="[
|
||||
{label: t('gymPage.home')},
|
||||
{label: t('gymPage.submit'), to: 'submit'},
|
||||
{label: t('gymPage.leaderboard'), to: 'leaderboard'}
|
||||
]"
|
||||
/>
|
||||
<q-btn
|
||||
:label="$t('gymPage.submit')"
|
||||
:to="getGymRoute(gym) + '/submit'"
|
||||
:color="submitPageSelected ? 'primary' : 'secondary'"
|
||||
/>
|
||||
<q-btn
|
||||
:label="$t('gymPage.leaderboard')"
|
||||
:to="getGymRoute(gym) + '/leaderboard'"
|
||||
:color="leaderboardPageSelected ? 'primary' : 'secondary'"
|
||||
/>
|
||||
</q-btn-group>
|
||||
<router-view />
|
||||
</StandardCenteredPage>
|
||||
</q-page>
|
||||
|
@ -30,9 +21,12 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import { getGymFromRoute, getGymRoute } from 'src/router/gym-routing';
|
||||
import { Gym } from 'src/api/main/gyms';
|
||||
import PageMenu from "components/PageMenu.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const t = useI18n().t;
|
||||
|
||||
const gym: Ref<Gym | undefined> = ref<Gym>();
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="user in followers" :key="user.id"
|
||||
:to="`/users/${user.id}`"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {User} from "src/api/main/auth";
|
||||
import {useAuthStore} from "stores/auth-store";
|
||||
import {onMounted, ref, Ref} from "vue";
|
||||
import api from 'src/api/main';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const authStore = useAuthStore();
|
||||
const followers: Ref<User[]> = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
followers.value = await api.auth.getFollowers(props.user.id, authStore, 0, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="user in following" :key="user.id"
|
||||
:to="`/users/${user.id}`"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {User} from "src/api/main/auth";
|
||||
import {useAuthStore} from "stores/auth-store";
|
||||
import {onMounted, ref, Ref} from "vue";
|
||||
import api from 'src/api/main';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const authStore = useAuthStore();
|
||||
const following: Ref<User[]> = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
following.value = await api.auth.getFollowing(props.user.id, authStore, 0, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<q-page>
|
||||
<StandardCenteredPage v-if="user">
|
||||
<h3>{{ user?.name }}</h3>
|
||||
|
||||
<div v-if="relationship">
|
||||
<q-btn v-if="!relationship.following" label="Follow" @click="followUser"/>
|
||||
<q-btn v-if="relationship.following" label="Unfollow" @click="unfollowUser"/>
|
||||
</div>
|
||||
|
||||
<PageMenu
|
||||
:base-route="`/users/${user.id}`"
|
||||
:items="[
|
||||
{label: 'Lifts', to: ''},
|
||||
{label: 'Followers', to: 'followers'},
|
||||
{label: 'Following', to: 'following'}
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Sub-pages are rendered here. -->
|
||||
<div v-if="userAccessible">
|
||||
<UserSubmissionsPage :user="user" v-if="route.path === getUserRoute(user)"/>
|
||||
<UserFollowersPage :user="user" v-if="route.path === getUserRoute(user) + '/followers'"/>
|
||||
<UserFollowingPage :user="user" v-if="route.path === getUserRoute(user) + '/following'"/>
|
||||
</div>
|
||||
|
||||
<div v-if="!userAccessible">
|
||||
This account is private.
|
||||
</div>
|
||||
|
||||
</StandardCenteredPage>
|
||||
<StandardCenteredPage v-if="userNotFound">
|
||||
<h3>{{ $t('userPage.notFound.title') }}</h3>
|
||||
<p>{{ $t('userPage.notFound.description') }}</p>
|
||||
</StandardCenteredPage>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import {onMounted, ref, Ref, watch} from 'vue';
|
||||
import {User, UserRelationship} from 'src/api/main/auth';
|
||||
import api from 'src/api/main';
|
||||
import {useRoute} from 'vue-router';
|
||||
import {useAuthStore} from 'stores/auth-store';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useQuasar} from 'quasar';
|
||||
import {showApiErrorToast} from 'src/utils';
|
||||
import PageMenu from 'components/PageMenu.vue';
|
||||
import UserSubmissionsPage from 'pages/user/UserSubmissionsPage.vue';
|
||||
import {getUserRoute} from 'src/router/user-routing';
|
||||
import UserFollowersPage from 'pages/user/UserFollowersPage.vue';
|
||||
import UserFollowingPage from 'pages/user/UserFollowingPage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const i18n = useI18n();
|
||||
const quasar = useQuasar();
|
||||
|
||||
const user: Ref<User | undefined> = ref();
|
||||
const relationship: Ref<UserRelationship | undefined> = ref();
|
||||
const isOwnUser = ref(false);
|
||||
const userNotFound = ref(false);
|
||||
const userAccessible = ref(false);
|
||||
|
||||
// If the user id changes, we have to manually reload the new user, since we
|
||||
// will end up on the same route component, which means the router won't
|
||||
// re-render.
|
||||
watch(route, async (updatedRoute) => {
|
||||
const userId = updatedRoute.params.userId[0];
|
||||
if (!user.value || user.value.id !== userId) {
|
||||
await loadUser(userId);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const userId = route.params.userId[0] as string;
|
||||
await loadUser(userId);
|
||||
});
|
||||
|
||||
async function loadUser(id: string) {
|
||||
try {
|
||||
user.value = await api.auth.getUser(id, authStore);
|
||||
isOwnUser.value = authStore.loggedIn && user.value.id === authStore.user?.id;
|
||||
userAccessible.value = await api.auth.isUserAccessible(id, authStore);
|
||||
await loadRelationship();
|
||||
} catch (error: any) {
|
||||
user.value = undefined;
|
||||
relationship.value = undefined;
|
||||
isOwnUser.value = false;
|
||||
userAccessible.value = false;
|
||||
if (error.response && error.response.status === 404) {
|
||||
userNotFound.value = true;
|
||||
} else {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRelationship() {
|
||||
if (authStore.user && user.value && userAccessible.value && authStore.user.id !== user.value.id) {
|
||||
try {
|
||||
relationship.value = await api.auth.getRelationshipTo(authStore.user.id, user.value.id, authStore);
|
||||
} catch (error) {
|
||||
relationship.value = undefined;
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
} else {
|
||||
relationship.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function followUser() {
|
||||
if (user.value) {
|
||||
try {
|
||||
await api.auth.followUser(user.value?.id, authStore);
|
||||
await loadRelationship();
|
||||
} catch (error) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function unfollowUser() {
|
||||
if (user.value) {
|
||||
try {
|
||||
await api.auth.unfollowUser(user.value?.id, authStore);
|
||||
await loadRelationship();
|
||||
} catch (error) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="recentSubmissions.length > 0">
|
||||
<q-list separator>
|
||||
<ExerciseSubmissionListItem
|
||||
v-for="sub in recentSubmissions"
|
||||
:submission="sub"
|
||||
:key="sub.id"
|
||||
:show-name="false"
|
||||
/>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useQuasar} from 'quasar';
|
||||
import {useAuthStore} from 'stores/auth-store';
|
||||
import {onMounted, ref, Ref} from 'vue';
|
||||
import {ExerciseSubmission} from 'src/api/main/submission';
|
||||
import {User} from 'src/api/main/auth';
|
||||
import api from 'src/api/main';
|
||||
import ExerciseSubmissionListItem from 'components/ExerciseSubmissionListItem.vue';
|
||||
import {showApiErrorToast} from 'src/utils';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const quasar = useQuasar();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const recentSubmissions: Ref<ExerciseSubmission[]> = ref([]);
|
||||
onMounted(async () => {
|
||||
try {
|
||||
recentSubmissions.value = await api.users.getRecentSubmissions(props.user.id, authStore);
|
||||
} catch (error: any) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -10,7 +10,7 @@ import RegisterPage from 'pages/auth/RegisterPage.vue';
|
|||
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 UserPage from 'pages/user/UserPage.vue';
|
||||
import UserSettingsPage from 'pages/auth/UserSettingsPage.vue';
|
||||
import UserSearchPage from 'pages/UserSearchPage.vue';
|
||||
|
||||
|
@ -28,15 +28,11 @@ const routes: RouteRecordRaw[] = [
|
|||
children: [
|
||||
{ path: '', component: GymSearchPage },
|
||||
{ path: 'users', component: UserSearchPage },
|
||||
{
|
||||
path: 'users/:userId',
|
||||
children: [
|
||||
{ path: '', component: UserPage },
|
||||
{ path: 'settings', component: UserSettingsPage }
|
||||
]
|
||||
{ path: 'users/:userId/settings', component: UserSettingsPage },
|
||||
{ // Match anything under /users/:userId to the UserPage, since it manages sub-pages manually.
|
||||
path: 'users/:userId+',
|
||||
component: UserPage
|
||||
},
|
||||
// { path: 'users/:userId', component: UserPage },
|
||||
// { path: 'users/:userId/settings', component: UserSettingsPage },
|
||||
{
|
||||
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
|
||||
component: GymPage,
|
||||
|
|
Loading…
Reference in New Issue