Skip to content
Open
84 changes: 82 additions & 2 deletions app/src/components/VRList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,25 @@
</button>
</template>
</div> -->
<div class="flex flex-col gap-3 md:flex-row md:items-center">
<input
v-model="searchQuery"
class="input input-bordered input-primary w-full md:max-w-sm"
placeholder="Sök VR-scen..."
/>

<select v-model="visibilityFilter" class="select select-bordered">
<option value="all">Alla</option>
<option value="owned">Äger</option>
<option value="member">Medlem</option>
<option value="public">Publika</option>
</select>
<button class="btn btn-sm" @click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'">
Sortera {{ sortOrder === 'asc' ? 'A → Ö' : 'Ö → A' }}
</button>
</div>
<div class="w-full grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-4 items-stretch">
<div v-for="space in listedVrSpaces" :key="space.vrSpaceId" class="card card-compact w-full shadow-xl">
<div v-for="space in paginatedVrSpaces" :key="space.vrSpaceId" class="card card-compact w-full shadow-xl">
<figure class="w-full h-32" style="background-size: cover"
:style="{ 'background-image': 'url(' + (space.image ? `${getAssetUrl(space.image)}?scaledown=8` : 'https://plus.unsplash.com/premium_photo-1663091704223-cc051e0f0c47?q=80&w=2073&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D') + ')' }" />
<div class="card-body bg-base-400 ">
Expand Down Expand Up @@ -45,13 +62,29 @@
</div>
</div>
</div>
<div class="flex items-center gap-2 mt-4">
<div class="flex gap-2">
<button class="btn btn-sm" :disabled="currentPage === 1" @click="currentPage--">
Föregående
</button>
<span class="text-sm leading-none self-center">Sida {{ currentPage }} av {{ totalPages }}</span>
<button class="btn btn-sm" :disabled="currentPage === totalPages" @click="currentPage++">
Nästa
</button>
</div>
<select v-model.number="pageSize" class="select select-bordered select-sm">
<option :value="12">12</option>
<option :value="24">24</option>
<option :value="48">48</option>
</select>
</div>
</div>
</template>
<script setup lang="ts">
import type { RouterOutputs } from '@/modules/trpcClient';
import { useConnectionStore } from '@/stores/connectionStore';
import { type VrSpaceId, hasAtLeastPermissionLevel, hasAtLeastSecurityRole, translatePermissionLevelAdjective, translatePermissionLevelVerb, translateVisibility } from 'schemas';
import { onBeforeMount, ref, computed } from 'vue';
import { onBeforeMount, ref, computed, watch } from 'vue';
import { RouterLink, useRouter } from 'vue-router';
import { getAssetUrl } from '@/modules/utils';
import { useAuthStore } from '@/stores/authStore';
Expand All @@ -60,6 +93,53 @@ const connection = useConnectionStore();
const authStore = useAuthStore();
// const vrSpaceStore = useVrSpaceStore();

const searchQuery = ref('');
const visibilityFilter = ref<'all' | 'owned' | 'member' | 'public'>('all');
const currentPage = ref(1);
const pageSize = ref(12);
const sortOrder = ref<'asc' | 'desc'>('asc');

const filteredVrSpaces = computed(() => {
const query = searchQuery.value.trim().toLowerCase();

return availableVrSpaces.value.filter(space => {
const matchesName =
query === '' || space.name.toLowerCase().includes(query);

const matchesVisibility =
visibilityFilter.value === 'all'
? true
: visibilityFilter.value === 'owned'
? space.permissionLevel === 'owner'
: visibilityFilter.value === 'member'
? ['admin', 'edit', 'view'].includes(space.permissionLevel ?? '')
: visibilityFilter.value === 'public'
? space.visibility === 'public'
: true;

return matchesName && matchesVisibility;
});
});

const sortedVrSpaces = computed(() => {
return [...filteredVrSpaces.value].sort((a, b) => {
return a.name.localeCompare(b.name, 'sv') * (sortOrder.value === 'asc' ? 1 : -1);
});
});

const totalPages = computed(() =>
Math.max(1, Math.ceil(sortedVrSpaces.value.length / pageSize.value))
);

const paginatedVrSpaces = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return sortedVrSpaces.value.slice(start, start + pageSize.value);
});

watch([searchQuery, visibilityFilter, pageSize], () => {
currentPage.value = 1;
});

const props = defineProps({
max: { type: Number, default: -1 },
});
Expand Down
71 changes: 66 additions & 5 deletions app/src/views/admin/AdminUserManagerView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@
<h3>
Befintliga användare
</h3>
<div class="flex flex-col gap-3 md:flex-row md:items-center">
<input
v-model="userSearch"
class="input input-bordered input-primary w-full md:max-w-sm"
placeholder="Sök användarnamn..."
/>

<button v-if="filterActive || userSearch" @click="clearFilters" class="btn btn-neutral btn-sm">
Rensa filter
</button>
</div>
<div class="flex flex-wrap gap-2">
<label v-for="role in fetchedRoles" class="btn btn-xs gap-1 rounded-full leading-none"
:class="`has-[:checked]:${getClassForRole(role.value)}`">
Expand All @@ -54,7 +65,7 @@
class="btn btn-neutral btn-circle btn-xs material-icons">clear</button>
</div>
<div class="grid grid-cols-[auto_auto_1fr_auto_auto] gap-x-4 gap-y-1">
<div v-for="user in filteredUsers" :key="user.userId" class="grid grid-cols-subgrid col-span-5 items-center">
<div v-for="user in paginatedUsers" :key="user.userId" class="grid grid-cols-subgrid col-span-5 items-center">
<template v-if="editedUserId === user.userId">
<input v-model="editedUsername" class="input input-bordered input-sm px-3 text-base font-bold"
placeholder="Användarnamn"
Expand Down Expand Up @@ -98,6 +109,25 @@
</template>
</div>
</div>
<div class="flex items-center gap-2 mt-4">
<button class="btn btn-sm" :disabled="currentPage === 1" @click="currentPage--">
Föregående
</button>

<span class="text-sm">
Sida {{ currentPage }} av {{ totalPages }}
</span>

<button class="btn btn-sm" :disabled="currentPage === totalPages" @click="currentPage++">
Nästa
</button>

<select v-model.number="pageSize" class="select select-bordered select-sm">
<option :value="10">10</option>
<option :value="25">25</option>
<option :value="50">50</option>
</select>
</div>
</div>
<div v-else>
<!-- Vad gör du här? Du får inte vara här. -->
Expand All @@ -109,7 +139,7 @@

<script setup lang="ts">
import { useAuthStore } from '@/stores/authStore';
import { computed, onBeforeMount, reactive, ref } from 'vue';
import { computed, onBeforeMount, reactive, ref, watch } from 'vue';
import { createUser, getAdmins, updateUser, deleteUser, getUsers } from '@/modules/authClient';
import { allRolesBelow, hasAtLeastSecurityRole, roleHierarchy, translateUserRole, type UserRole } from 'schemas';
import MaxWidth7xl from '@/components/layout/MaxWidth7xl.vue';
Expand Down Expand Up @@ -143,6 +173,10 @@ function getClassForRole(role: UserRole) {
}
}

const userSearch = ref('');
const currentPage = ref(1);
const pageSize = ref(10);

const createdUsername = ref('');
const createdPassword = ref('');
const createdRole = ref<UserRole>('guest');
Expand Down Expand Up @@ -173,14 +207,41 @@ const filterActive = computed(() => {
return filterArr.some(([k, active]) => active);
})
function clearFilters() {
userSearch.value = '';
for (const key of Object.keys(roleFilter) as UserRole[]) {
roleFilter[key] = false;
}
}
const filteredUsers = computed(() => {
if (!filterActive.value) return fetchedUsers.value;
return fetchedUsers.value.filter(user => pickedRoleFilters.value.includes(user.role));
})
const query = userSearch.value.trim().toLowerCase();

const activeRoles = Object.entries(roleFilter)
.filter(([, active]) => active)
.map(([role]) => role as UserRole);

return fetchedUsers.value.filter(user => {
const matchesSearch =
query === '' || user.username.toLowerCase().includes(query);

const matchesRole =
activeRoles.length === 0 || activeRoles.includes(user.role);

return matchesSearch && matchesRole;
});
});

const totalPages = computed(() =>
Math.max(1, Math.ceil(filteredUsers.value.length / pageSize.value))
);

const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return filteredUsers.value.slice(start, start + pageSize.value);
});

watch([userSearch, roleFilter], () => {
currentPage.value = 1;
}, { deep: true });

const editedUserId = ref<string>();
const editedUsername = ref<string>();
Expand Down
4 changes: 2 additions & 2 deletions app/src/views/public/StartView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</h2>
<button v-if="canCreateVrSpace" class="btn btn-primary btn-outline btn-sm px-2" @click="createVrSpace">
<span class="material-icons">add</span>
Skapa ny VR-scen
Skapa ny VR-miljö
</button>
</div>
<VRList />
Expand All @@ -39,7 +39,7 @@ const authStore = useAuthStore();
const vrSpaceStore = useVrSpaceStore();

const canCreateVrSpace = computed(() => {
if (!authStore.role) { return false; }
if (!authStore.role || !hasAtLeastSecurityRole(authStore.role, 'admin')) { return false; }
return hasAtLeastSecurityRole(authStore.role, 'user');
});
async function createVrSpace() {
Expand Down