@vue-skuilder/common-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index.css +10 -0
- package/dist/common-ui.es.js +16404 -0
- package/dist/common-ui.es.js.map +1 -0
- package/dist/common-ui.umd.js +9 -0
- package/dist/common-ui.umd.js.map +1 -0
- package/dist/components/HeatMap.types.d.ts +13 -0
- package/dist/components/PaginatingToolbar.types.d.ts +40 -0
- package/dist/components/SkMouseTrap.types.d.ts +3 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
- package/dist/components/SnackbarService.d.ts +11 -0
- package/dist/components/StudySession.types.d.ts +6 -0
- package/dist/components/auth/index.d.ts +4 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
- package/dist/composables/CompositionViewable.d.ts +33 -0
- package/dist/composables/Displayable.d.ts +47 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/index.d.ts +36 -0
- package/dist/plugins/pinia.d.ts +5 -0
- package/dist/stores/useAuthStore.d.ts +225 -0
- package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
- package/dist/stores/useConfigStore.d.ts +11 -0
- package/dist/utils/SkldrMouseTrap.d.ts +32 -0
- package/package.json +67 -0
- package/src/components/HeatMap.types.ts +15 -0
- package/src/components/HeatMap.vue +354 -0
- package/src/components/PaginatingToolbar.types.ts +48 -0
- package/src/components/PaginatingToolbar.vue +75 -0
- package/src/components/SkMouseTrap.types.ts +3 -0
- package/src/components/SkMouseTrap.vue +70 -0
- package/src/components/SkMouseTrapToolTip.types.ts +41 -0
- package/src/components/SkMouseTrapToolTip.vue +316 -0
- package/src/components/SnackbarService.ts +39 -0
- package/src/components/SnackbarService.vue +71 -0
- package/src/components/StudySession.types.ts +6 -0
- package/src/components/StudySession.vue +670 -0
- package/src/components/StudySessionTimer.vue +121 -0
- package/src/components/auth/UserChip.vue +106 -0
- package/src/components/auth/UserLogin.vue +141 -0
- package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
- package/src/components/auth/UserRegistration.vue +181 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
- package/src/components/cardRendering/CardLoader.vue +123 -0
- package/src/components/cardRendering/CardViewer.vue +101 -0
- package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
- package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
- package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
- package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
- package/src/components/studentInputs/BaseUserInput.ts +71 -0
- package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
- package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
- package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
- package/src/components/studentInputs/TrueFalse.vue +27 -0
- package/src/components/studentInputs/UserInputNumber.vue +63 -0
- package/src/components/studentInputs/UserInputString.vue +89 -0
- package/src/components/studentInputs/fillInInput.vue +71 -0
- package/src/composables/CompositionViewable.ts +180 -0
- package/src/composables/Displayable.ts +133 -0
- package/src/composables/index.ts +2 -0
- package/src/index.ts +79 -0
- package/src/plugins/pinia.ts +24 -0
- package/src/stores/useAuthStore.ts +92 -0
- package/src/stores/useCardPreviewModeStore.ts +32 -0
- package/src/stores/useConfigStore.ts +60 -0
- package/src/utils/SkldrMouseTrap.ts +141 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<!-- @vue-skuilder/common-ui/src/components/StudySessionTimer.vue -->
|
|
2
|
+
<template>
|
|
3
|
+
<v-tooltip location="right" :open-delay="0" :close-delay="200" color="secondary" class="text-subtitle-1">
|
|
4
|
+
<template #activator="{ props }">
|
|
5
|
+
<div class="timer-container" v-bind="props" @mouseenter="hovered = true" @mouseleave="hovered = false">
|
|
6
|
+
<v-progress-circular
|
|
7
|
+
alt="Time remaining in study session"
|
|
8
|
+
size="64"
|
|
9
|
+
width="8"
|
|
10
|
+
rotate="0"
|
|
11
|
+
:color="timerColor"
|
|
12
|
+
:model-value="percentageRemaining"
|
|
13
|
+
>
|
|
14
|
+
<v-btn
|
|
15
|
+
v-if="timeRemaining > 0 && hovered"
|
|
16
|
+
icon
|
|
17
|
+
color="transparent"
|
|
18
|
+
location="bottom left"
|
|
19
|
+
@click="addSessionTime"
|
|
20
|
+
>
|
|
21
|
+
<v-icon size="large">mdi-plus</v-icon>
|
|
22
|
+
</v-btn>
|
|
23
|
+
</v-progress-circular>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
{{ formattedTimeRemaining }}
|
|
27
|
+
</v-tooltip>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { defineComponent, computed, ref } from 'vue';
|
|
32
|
+
|
|
33
|
+
export default defineComponent({
|
|
34
|
+
name: 'StudySessionTimer',
|
|
35
|
+
|
|
36
|
+
props: {
|
|
37
|
+
/**
|
|
38
|
+
* Time remaining in seconds
|
|
39
|
+
*/
|
|
40
|
+
timeRemaining: {
|
|
41
|
+
type: Number,
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Total session time limit in minutes
|
|
46
|
+
*/
|
|
47
|
+
sessionTimeLimit: {
|
|
48
|
+
type: Number,
|
|
49
|
+
required: true,
|
|
50
|
+
default: 5,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
emits: ['add-time'],
|
|
55
|
+
|
|
56
|
+
setup(props, { emit }) {
|
|
57
|
+
const hovered = ref(false);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Formats the time remaining into a readable string
|
|
61
|
+
*/
|
|
62
|
+
const formattedTimeRemaining = computed(() => {
|
|
63
|
+
let timeString = '';
|
|
64
|
+
const seconds = props.timeRemaining;
|
|
65
|
+
|
|
66
|
+
if (seconds > 60) {
|
|
67
|
+
timeString = Math.floor(seconds / 60).toString() + ':';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const secondsRemaining = seconds % 60;
|
|
71
|
+
timeString += secondsRemaining >= 10 ? secondsRemaining : '0' + secondsRemaining;
|
|
72
|
+
|
|
73
|
+
if (seconds <= 60) {
|
|
74
|
+
timeString += ' seconds';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
timeString += ' left!';
|
|
78
|
+
|
|
79
|
+
return timeString;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculates the percentage of time remaining for the progress indicator
|
|
84
|
+
*/
|
|
85
|
+
const percentageRemaining = computed(() => {
|
|
86
|
+
return props.timeRemaining > 60
|
|
87
|
+
? 100 * (props.timeRemaining / (60 * props.sessionTimeLimit))
|
|
88
|
+
: 100 * (props.timeRemaining / 60);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Determines the color of the timer based on time remaining
|
|
93
|
+
*/
|
|
94
|
+
const timerColor = computed(() => {
|
|
95
|
+
return props.timeRemaining > 60 ? 'primary' : 'orange darken-3';
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handles adding time to the session
|
|
100
|
+
*/
|
|
101
|
+
const addSessionTime = () => {
|
|
102
|
+
emit('add-time');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
hovered,
|
|
107
|
+
formattedTimeRemaining,
|
|
108
|
+
percentageRemaining,
|
|
109
|
+
timerColor,
|
|
110
|
+
addSessionTime,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.timer-container {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-badge :content="items.length" :model-value="hasNewItems" color="accent" location="end top">
|
|
3
|
+
<v-menu location="bottom end" transition="scale-transition">
|
|
4
|
+
<template #activator="{ props }">
|
|
5
|
+
<v-chip v-bind="props" class="ma-2">
|
|
6
|
+
<v-avatar start class="bg-primary">
|
|
7
|
+
<v-icon>mdi-school</v-icon>
|
|
8
|
+
</v-avatar>
|
|
9
|
+
{{ username }}
|
|
10
|
+
</v-chip>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<v-list>
|
|
14
|
+
<v-list-item v-for="item in items" :key="item" @click="dismiss(item)">
|
|
15
|
+
<v-list-item-title>{{ item }}</v-list-item-title>
|
|
16
|
+
</v-list-item>
|
|
17
|
+
|
|
18
|
+
<v-divider v-if="items.length" />
|
|
19
|
+
|
|
20
|
+
<v-list-item @click="gotoStats">
|
|
21
|
+
<template #prepend>
|
|
22
|
+
<v-icon>mdi-trending-up</v-icon>
|
|
23
|
+
</template>
|
|
24
|
+
<v-list-item-title>Stats</v-list-item-title>
|
|
25
|
+
</v-list-item>
|
|
26
|
+
|
|
27
|
+
<v-list-item @click="gotoSettings">
|
|
28
|
+
<template #prepend>
|
|
29
|
+
<v-icon>mdi-cog</v-icon>
|
|
30
|
+
</template>
|
|
31
|
+
<v-list-item-title>Settings</v-list-item-title>
|
|
32
|
+
</v-list-item>
|
|
33
|
+
|
|
34
|
+
<v-list-item @click="logout">
|
|
35
|
+
<template #prepend>
|
|
36
|
+
<v-icon>mdi-logout</v-icon>
|
|
37
|
+
</template>
|
|
38
|
+
<v-list-item-title>Log out</v-list-item-title>
|
|
39
|
+
</v-list-item>
|
|
40
|
+
</v-list>
|
|
41
|
+
</v-menu>
|
|
42
|
+
</v-badge>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script lang="ts">
|
|
46
|
+
import { defineComponent } from 'vue';
|
|
47
|
+
import { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';
|
|
48
|
+
import { useConfigStore } from '../../stores/useConfigStore';
|
|
49
|
+
|
|
50
|
+
export default defineComponent({
|
|
51
|
+
name: 'UserChip',
|
|
52
|
+
|
|
53
|
+
data() {
|
|
54
|
+
return {
|
|
55
|
+
username: '',
|
|
56
|
+
items: [] as string[],
|
|
57
|
+
checked: false,
|
|
58
|
+
authStore: useAuthStore(),
|
|
59
|
+
configStore: useConfigStore(),
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
computed: {
|
|
64
|
+
hasNewItems(): boolean {
|
|
65
|
+
return this.items.length > 0;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
created() {
|
|
70
|
+
getCurrentUser().then((u) => {
|
|
71
|
+
this.username = u.getUsername();
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
methods: {
|
|
76
|
+
async gotoSettings() {
|
|
77
|
+
this.$router.push(`/u/${(await getCurrentUser()).getUsername()}`);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async gotoStats() {
|
|
81
|
+
this.$router.push(`/u/${(await getCurrentUser()).getUsername()}/stats`);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
dismiss(item: string) {
|
|
85
|
+
const index = this.items.indexOf(item);
|
|
86
|
+
this.items.splice(index, 1);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async logout() {
|
|
90
|
+
const res = await this.authStore._user!.logout();
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
this.authStore.loginAndRegistration = {
|
|
93
|
+
init: true,
|
|
94
|
+
loggedIn: false,
|
|
95
|
+
regDialogOpen: false,
|
|
96
|
+
loginDialogOpen: false,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
this.configStore.resetDefaults();
|
|
100
|
+
|
|
101
|
+
this.$router.push('/home');
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card>
|
|
3
|
+
<v-card-title v-if="!loginRoute" class="text-h5 bg-grey-lighten-2">Log In</v-card-title>
|
|
4
|
+
|
|
5
|
+
<v-card-text>
|
|
6
|
+
<v-form onsubmit="return false;" @submit.prevent="login">
|
|
7
|
+
<v-text-field
|
|
8
|
+
id=""
|
|
9
|
+
v-model="username"
|
|
10
|
+
autofocus
|
|
11
|
+
name="username"
|
|
12
|
+
label="Username"
|
|
13
|
+
prepend-icon="mdi-account-circle"
|
|
14
|
+
></v-text-field>
|
|
15
|
+
<v-text-field
|
|
16
|
+
v-model="password"
|
|
17
|
+
prepend-icon="mdi-lock"
|
|
18
|
+
name="password"
|
|
19
|
+
hover="Show password input"
|
|
20
|
+
label="Enter your password"
|
|
21
|
+
hint=""
|
|
22
|
+
min="0"
|
|
23
|
+
:append-icon="passwordVisible ? 'mdi-eye-off' : 'mdi-eye'"
|
|
24
|
+
:type="passwordVisible ? 'text' : 'password'"
|
|
25
|
+
@click:append="() => (passwordVisible = !passwordVisible)"
|
|
26
|
+
></v-text-field>
|
|
27
|
+
|
|
28
|
+
<v-snackbar v-model="badLoginAttempt" location="bottom right" :timeout="errorTimeout">
|
|
29
|
+
Username or password was incorrect.
|
|
30
|
+
<v-btn color="pink" variant="text" @click="badLoginAttempt = false">Close</v-btn>
|
|
31
|
+
</v-snackbar>
|
|
32
|
+
|
|
33
|
+
<v-btn class="mr-2" type="submit" :loading="awaitingResponse" :color="buttonStatus.color">
|
|
34
|
+
<v-icon start>mdi-lock-open</v-icon>
|
|
35
|
+
Log In
|
|
36
|
+
</v-btn>
|
|
37
|
+
<router-link v-if="loginRoute" to="signup">
|
|
38
|
+
<v-btn variant="text">Create New Account</v-btn>
|
|
39
|
+
</router-link>
|
|
40
|
+
<v-btn v-else variant="text" @click="toggle">Create New Account</v-btn>
|
|
41
|
+
</v-form>
|
|
42
|
+
</v-card-text>
|
|
43
|
+
</v-card>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script lang="ts" setup>
|
|
47
|
+
import { ref, computed } from 'vue';
|
|
48
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
49
|
+
import { alertUser } from '../SnackbarService';
|
|
50
|
+
import { log } from '@vue-skuilder/common';
|
|
51
|
+
import { Status } from '@vue-skuilder/common';
|
|
52
|
+
import { User } from '@vue-skuilder/db';
|
|
53
|
+
import { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';
|
|
54
|
+
import { useConfigStore } from '../../stores/useConfigStore';
|
|
55
|
+
|
|
56
|
+
const router = useRouter();
|
|
57
|
+
const route = useRoute();
|
|
58
|
+
const authStore = useAuthStore();
|
|
59
|
+
const configStore = useConfigStore();
|
|
60
|
+
|
|
61
|
+
const username = ref('');
|
|
62
|
+
const password = ref('');
|
|
63
|
+
const passwordVisible = ref(false);
|
|
64
|
+
const awaitingResponse = ref(false);
|
|
65
|
+
const badLoginAttempt = ref(false);
|
|
66
|
+
const errorTimeout = ref(7000);
|
|
67
|
+
const user = ref<User | undefined>(undefined);
|
|
68
|
+
|
|
69
|
+
const loginRoute = computed(() => route.name === 'login');
|
|
70
|
+
|
|
71
|
+
const buttonStatus = computed(() => ({
|
|
72
|
+
color: badLoginAttempt.value ? 'error' : 'success',
|
|
73
|
+
text: badLoginAttempt.value ? 'Try again' : 'Log In',
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const initBadLogin = () => {
|
|
77
|
+
badLoginAttempt.value = true;
|
|
78
|
+
|
|
79
|
+
alertUser({
|
|
80
|
+
text: 'Username or password was incorrect.',
|
|
81
|
+
status: Status.error,
|
|
82
|
+
timeout: errorTimeout.value,
|
|
83
|
+
});
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
badLoginAttempt.value = false;
|
|
86
|
+
}, errorTimeout.value);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const login = async () => {
|
|
90
|
+
awaitingResponse.value = true;
|
|
91
|
+
log('Starting login attempt');
|
|
92
|
+
log(`Login attempt for username: ${username.value}`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
log('Attempting to get User instance');
|
|
96
|
+
// #172 starting point - why is the pre-existing _user being referenced here?
|
|
97
|
+
user.value = await getCurrentUser();
|
|
98
|
+
log('Got User instance, attempting login');
|
|
99
|
+
|
|
100
|
+
await user.value.login(username.value, password.value);
|
|
101
|
+
log('Login successful');
|
|
102
|
+
|
|
103
|
+
// load user config
|
|
104
|
+
log('Initializing user config');
|
|
105
|
+
configStore.init();
|
|
106
|
+
log('User config initialized');
|
|
107
|
+
|
|
108
|
+
// set login state
|
|
109
|
+
log('Setting authentication state');
|
|
110
|
+
authStore.loginAndRegistration.loggedIn = true;
|
|
111
|
+
log('Authentication state set, redirecting to study page');
|
|
112
|
+
router.push('/study');
|
|
113
|
+
log('Login and redirect complete');
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// entry #186
|
|
116
|
+
log('Login attempt failed');
|
|
117
|
+
log(`Login error details: ${JSON.stringify(e)}`);
|
|
118
|
+
console.log(`login error: ${JSON.stringify(e)}`);
|
|
119
|
+
// - differentiate response
|
|
120
|
+
// - return better message to UI
|
|
121
|
+
log('Initiating bad login feedback');
|
|
122
|
+
initBadLogin();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
log('Resetting awaiting response state');
|
|
126
|
+
awaitingResponse.value = false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const emit = defineEmits(['toggle']);
|
|
130
|
+
|
|
131
|
+
const toggle = () => {
|
|
132
|
+
log('Toggling registration / login forms.');
|
|
133
|
+
emit('toggle');
|
|
134
|
+
};
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<style lang="css" scoped>
|
|
138
|
+
.login {
|
|
139
|
+
max-width: 650px;
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition v-if="userReady && display" name="component-fade" mode="out-in">
|
|
3
|
+
<div v-if="guestMode">
|
|
4
|
+
<v-dialog v-model="regDialog" width="500px">
|
|
5
|
+
<template #activator="{ props }">
|
|
6
|
+
<v-btn class="mr-2" size="small" color="success" v-bind="props">Sign Up</v-btn>
|
|
7
|
+
</template>
|
|
8
|
+
<UserRegistration @toggle="toggle" />
|
|
9
|
+
</v-dialog>
|
|
10
|
+
<v-dialog v-model="loginDialog" width="500px">
|
|
11
|
+
<template #activator="{ props }">
|
|
12
|
+
<v-btn size="small" color="success" v-bind="props">Log In</v-btn>
|
|
13
|
+
</template>
|
|
14
|
+
<UserLogin @toggle="toggle" />
|
|
15
|
+
</v-dialog>
|
|
16
|
+
</div>
|
|
17
|
+
<user-chip v-else />
|
|
18
|
+
</transition>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script lang="ts" setup>
|
|
22
|
+
import { computed } from 'vue';
|
|
23
|
+
import { useRoute } from 'vue-router';
|
|
24
|
+
import UserLogin from './UserLogin.vue';
|
|
25
|
+
import UserRegistration from './UserRegistration.vue';
|
|
26
|
+
import UserChip from './UserChip.vue';
|
|
27
|
+
import { useAuthStore } from '../../stores/useAuthStore';
|
|
28
|
+
import { GuestUsername } from '@vue-skuilder/db';
|
|
29
|
+
|
|
30
|
+
const route = useRoute();
|
|
31
|
+
const authStore = useAuthStore();
|
|
32
|
+
|
|
33
|
+
const display = computed(() => {
|
|
34
|
+
if (!route.name || typeof route.name !== 'string') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const routeName = route.name.toLowerCase();
|
|
38
|
+
return !(routeName === 'login' || routeName === 'signup');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const userReady = computed(() => authStore.onLoadComplete);
|
|
42
|
+
|
|
43
|
+
const guestMode = computed(() => {
|
|
44
|
+
if (authStore._user) {
|
|
45
|
+
return authStore._user.getUsername().startsWith(GuestUsername);
|
|
46
|
+
}
|
|
47
|
+
return !authStore.loginAndRegistration.loggedIn;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const regDialog = computed({
|
|
51
|
+
get: () => authStore.loginAndRegistration.regDialogOpen,
|
|
52
|
+
set: (value: boolean) => {
|
|
53
|
+
authStore.loginAndRegistration.regDialogOpen = value;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const loginDialog = computed({
|
|
58
|
+
get: () => authStore.loginAndRegistration.loginDialogOpen,
|
|
59
|
+
set: (value: boolean) => {
|
|
60
|
+
authStore.loginAndRegistration.loginDialogOpen = value;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const toggle = () => {
|
|
65
|
+
if (regDialog.value && loginDialog.value) {
|
|
66
|
+
throw new Error('Registration / Login dialogs both activated.');
|
|
67
|
+
} else if (regDialog.value === loginDialog.value) {
|
|
68
|
+
throw new Error('Registration / Login dialogs toggled while both were dormant.');
|
|
69
|
+
} else {
|
|
70
|
+
regDialog.value = !regDialog.value;
|
|
71
|
+
loginDialog.value = !loginDialog.value;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<style scoped>
|
|
77
|
+
.component-fade-enter-active,
|
|
78
|
+
.component-fade-leave-active {
|
|
79
|
+
transition: opacity 0.5s ease;
|
|
80
|
+
}
|
|
81
|
+
.component-fade-enter,
|
|
82
|
+
.component-fade-leave-to {
|
|
83
|
+
opacity: 0;
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card>
|
|
3
|
+
<v-card-title v-if="!registrationRoute" class="text-h5 bg-grey-lighten-2"> Create an Account </v-card-title>
|
|
4
|
+
|
|
5
|
+
<v-card-text>
|
|
6
|
+
<v-form @submit.prevent="createUser">
|
|
7
|
+
<v-text-field
|
|
8
|
+
id=""
|
|
9
|
+
ref="userNameTextField"
|
|
10
|
+
v-model="username"
|
|
11
|
+
autofocus
|
|
12
|
+
name="username"
|
|
13
|
+
label="Choose a Username"
|
|
14
|
+
prepend-icon="mdi-account-circle"
|
|
15
|
+
:error="usernameError"
|
|
16
|
+
:hint="usernameHint"
|
|
17
|
+
@blur="validateUsername"
|
|
18
|
+
></v-text-field>
|
|
19
|
+
<v-text-field
|
|
20
|
+
v-model="password"
|
|
21
|
+
prepend-icon="mdi-lock"
|
|
22
|
+
name="password"
|
|
23
|
+
hover="Show password"
|
|
24
|
+
label="Create a password"
|
|
25
|
+
hint=""
|
|
26
|
+
min="4"
|
|
27
|
+
:append-icon="passwordVisible ? 'mdi-eye-off' : 'mdi-eye'"
|
|
28
|
+
:type="passwordVisible ? 'text' : 'password'"
|
|
29
|
+
@click:append="() => (passwordVisible = !passwordVisible)"
|
|
30
|
+
></v-text-field>
|
|
31
|
+
<v-text-field
|
|
32
|
+
v-model="retypedPassword"
|
|
33
|
+
prepend-icon="mdi-lock"
|
|
34
|
+
name="retypedPassword"
|
|
35
|
+
hover="Show password"
|
|
36
|
+
label="Retype your password"
|
|
37
|
+
hint=""
|
|
38
|
+
min="4"
|
|
39
|
+
:type="passwordVisible ? 'text' : 'password'"
|
|
40
|
+
></v-text-field>
|
|
41
|
+
|
|
42
|
+
<!-- <v-checkbox label="Student" v-model="student" ></v-checkbox>
|
|
43
|
+
<v-checkbox label="Teacher" v-model="teacher" ></v-checkbox>
|
|
44
|
+
<v-checkbox label="Author" v-model="author" ></v-checkbox> -->
|
|
45
|
+
|
|
46
|
+
<v-snackbar v-model="badLoginAttempt" location="bottom right" :timeout="5000">
|
|
47
|
+
Username or password was incorrect.
|
|
48
|
+
<v-btn color="pink" variant="text" @click="badLoginAttempt = false"> Close </v-btn>
|
|
49
|
+
</v-snackbar>
|
|
50
|
+
<v-btn class="mr-2" type="submit" :loading="awaitingResponse" :color="buttonStatus.color">
|
|
51
|
+
<v-icon start>mdi-lock-open</v-icon>
|
|
52
|
+
Create Account
|
|
53
|
+
</v-btn>
|
|
54
|
+
<router-link v-if="registrationRoute" to="login">
|
|
55
|
+
<v-btn variant="text">Log In</v-btn>
|
|
56
|
+
</router-link>
|
|
57
|
+
<v-btn v-else variant="text" @click="toggle"> Log In </v-btn>
|
|
58
|
+
</v-form>
|
|
59
|
+
</v-card-text>
|
|
60
|
+
</v-card>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script lang="ts">
|
|
64
|
+
import { defineComponent } from 'vue';
|
|
65
|
+
import { UserDBInterface } from '@vue-skuilder/db';
|
|
66
|
+
import { alertUser } from '../SnackbarService';
|
|
67
|
+
import { Status, log } from '@vue-skuilder/common';
|
|
68
|
+
import { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';
|
|
69
|
+
|
|
70
|
+
export default defineComponent({
|
|
71
|
+
name: 'UserRegistration',
|
|
72
|
+
|
|
73
|
+
emits: ['toggle'],
|
|
74
|
+
|
|
75
|
+
data() {
|
|
76
|
+
return {
|
|
77
|
+
username: '',
|
|
78
|
+
password: '',
|
|
79
|
+
retypedPassword: '',
|
|
80
|
+
passwordVisible: false,
|
|
81
|
+
usernameValidationInProgress: false,
|
|
82
|
+
usernameError: false,
|
|
83
|
+
usernameHint: '',
|
|
84
|
+
awaitingResponse: false,
|
|
85
|
+
badLoginAttempt: false,
|
|
86
|
+
userSecret: '',
|
|
87
|
+
secret: 'goons',
|
|
88
|
+
user: null as UserDBInterface | null,
|
|
89
|
+
roles: ['Student', 'Teacher', 'Author'] as string[],
|
|
90
|
+
student: true,
|
|
91
|
+
teacher: false,
|
|
92
|
+
author: false,
|
|
93
|
+
authStore: useAuthStore(),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
computed: {
|
|
98
|
+
registrationRoute(): boolean {
|
|
99
|
+
return typeof this.$route.name === 'string' && this.$route.name.toLowerCase() === 'signup';
|
|
100
|
+
},
|
|
101
|
+
buttonStatus() {
|
|
102
|
+
return {
|
|
103
|
+
color: this.badLoginAttempt ? 'error' : 'success',
|
|
104
|
+
text: this.badLoginAttempt ? 'Try again' : 'Log In',
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async created() {
|
|
110
|
+
this.user = await getCurrentUser();
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
methods: {
|
|
114
|
+
toggle() {
|
|
115
|
+
log('Toggling registration / login forms.');
|
|
116
|
+
this.$emit('toggle');
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
validateUsername() {
|
|
120
|
+
this.usernameError = false;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async createUser() {
|
|
124
|
+
this.awaitingResponse = true;
|
|
125
|
+
log(`
|
|
126
|
+
User creation
|
|
127
|
+
-------------
|
|
128
|
+
|
|
129
|
+
Name: ${this.username}
|
|
130
|
+
Student: ${this.student}
|
|
131
|
+
Teacher: ${this.teacher}
|
|
132
|
+
Author: ${this.author}
|
|
133
|
+
`);
|
|
134
|
+
if (this.password === this.retypedPassword) {
|
|
135
|
+
if (!this.user) return;
|
|
136
|
+
|
|
137
|
+
this.user
|
|
138
|
+
.createAccount(this.username, this.password)
|
|
139
|
+
.then(async (resp) => {
|
|
140
|
+
if (resp.status === Status.ok) {
|
|
141
|
+
this.authStore.loginAndRegistration.loggedIn = true;
|
|
142
|
+
this.authStore.loginAndRegistration.init = false;
|
|
143
|
+
this.authStore.loginAndRegistration.init = true;
|
|
144
|
+
|
|
145
|
+
this.$router.push(`/u/${(await getCurrentUser()).getUsername()}/new`);
|
|
146
|
+
} else {
|
|
147
|
+
if (resp.error === 'This username is taken!') {
|
|
148
|
+
this.usernameError = true;
|
|
149
|
+
this.usernameHint = 'Try a different name.';
|
|
150
|
+
(this.$refs.userNameTextField as HTMLInputElement).focus();
|
|
151
|
+
alertUser({
|
|
152
|
+
text: `The name ${this.username} is taken!`,
|
|
153
|
+
status: resp.status,
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
alertUser({
|
|
157
|
+
text: resp.error,
|
|
158
|
+
status: resp.status,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
.catch((e) => {
|
|
164
|
+
if (e)
|
|
165
|
+
alertUser({
|
|
166
|
+
text: JSON.stringify(e),
|
|
167
|
+
status: Status.error,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
this.awaitingResponse = false;
|
|
171
|
+
} else {
|
|
172
|
+
alertUser({
|
|
173
|
+
text: 'Passwords do not match.',
|
|
174
|
+
status: Status.error,
|
|
175
|
+
});
|
|
176
|
+
this.awaitingResponse = false;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
</script>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as UserChip } from './UserChip.vue';
|
|
2
|
+
export { default as UserLogin } from './UserLogin.vue';
|
|
3
|
+
export { default as UserRegistration } from './UserRegistration.vue';
|
|
4
|
+
export { default as UserLoginAndRegistrationContainer } from './UserLoginAndRegistrationContainer.vue';
|