diff --git a/src/components/HistoryBox.vue b/src/components/HistoryBox.vue index 53d97059860c2faa7397fdee8ef2109664fbc117..f6df71f47c308d1da035a873783979f4af21f1ad 100644 --- a/src/components/HistoryBox.vue +++ b/src/components/HistoryBox.vue @@ -9,6 +9,7 @@ const props = defineProps({ username: String, difficulty: String, score: Number, + date: String, }); @@ -17,7 +18,8 @@ const props = defineProps({ <template> <div class="quiz-box"> <div class="quiz-info"> - <img src="/src/assets/trivio.svg" alt="Question picture"> + <img v-if="image" :src="image" alt="trivio image"> + <img v-else src="/src/assets/trivio.svg" alt="no picture found"> <div class="quiz-text"> <div class="quiz-title">{{title}}</div> <div class="difficulty">Difficulty: {{difficulty}}</div> @@ -28,7 +30,7 @@ const props = defineProps({ </div> <div class="right-side"> <H1 class="score-text">Score: {{ score }}/{{ numberOfQuestion }}</H1> - <H3 class="date">Date: 04.03.24</h3> + <H3 class="date">{{date}}</h3> </div> </div> diff --git a/src/components/TrivioQuestion.vue b/src/components/TrivioQuestion.vue index 955800eae989d6d38d9d23a11d59ec23cf645c05..7c6ba8063b4c239cad0f511c7b24172e8339deaa 100644 --- a/src/components/TrivioQuestion.vue +++ b/src/components/TrivioQuestion.vue @@ -6,6 +6,7 @@ const props = defineProps<{ index:number; questionId:number; question: questionData; + blobUrl: string; }>(); const tokenStore = useTokenStore() @@ -26,7 +27,7 @@ const answers = ref<Answer[]>(questionType.value === 'multiple' ] ); const tags = ref<string[]>(props.question && props.question.tags ? [...props.question.tags] : []); - +const changed = ref(false) const media = ref( props.question? props.question.media || props.question.media: "") const blobUrl = ref("") @@ -82,6 +83,7 @@ const handleFileUpload = async () => { const uploadedImage = await uploadPicture(file, tokenStore.jwtToken); const uploadedImageUrl = await getPicture(uploadedImage, tokenStore.jwtToken) media.value = uploadedImage; + changed.value = true blobUrl.value = uploadedImageUrl console.log('Uploaded picture URL:', uploadedImageUrl); console.log(uploadedImageUrl); @@ -174,7 +176,8 @@ watch(media, saveQuestionData, {deep: true}) <div class="image-box"> <label> <input type="file" style="display: none" ref="fileInput" accept="image/png, image/jpeg" @change="handleFileUpload"> - <img v-if="media" :src="blobUrl" alt="Uploaded Image" width="150px" height="150px"> + <img v-if="media && !changed" :src="props.blobUrl" alt="Uploaded Image" width="150px" height="150px"> + <img v-else-if="media && changed" :src="blobUrl" alt="Uploaded Image" width="150px" height="150px"> <img v-else src="/src/components/icons/AddImage.png" alt="Add Image" width="50px" height="50px" @click="handleDefaultImageClick"> </label> </div> diff --git a/src/main.ts b/src/main.ts index b7fc8e7a49dead55a6e4d5113cb531a3083dd189..1f28a364bf6d0b5fad1b0220dbe85303cbf6739b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import './assets/main.css' import piniaPluginPersistedState from "pinia-plugin-persistedstate" -import { createApp } from 'vue' +import {createApp, markRaw} from 'vue' import { createPinia } from 'pinia' import App from './App.vue' @@ -10,6 +10,9 @@ import router from './router' const app = createApp(App) const pinia = createPinia(); pinia.use(piniaPluginPersistedState) +pinia.use(({ store }) => { + store.router = markRaw(router) +}) app.use(pinia) app.use(router) diff --git a/src/stores/token.ts b/src/stores/token.ts index cfb627e9aa591b2265cfdbea115a31a0d2eff34a..93dfa0da73faf028a621334998ceddf5bea2aa15 100644 --- a/src/stores/token.ts +++ b/src/stores/token.ts @@ -1,14 +1,19 @@ -import axios from "axios"; + import axios from "axios"; import { defineStore } from "pinia"; import {getJWTToken} from "@/utils/httputils"; import { jwtDecode } from "jwt-decode"; + import router from "@/router"; + import {useRouter} from "vue-router"; + import {markRaw} from "vue"; -export const useTokenStore = defineStore({ +export const useTokenStore = defineStore( { id: 'token', state: () => ({ jwtToken: "", loggedInUser: "", - password: "" + password: "", + tokenExpirationTimer: null as number | null, + router: markRaw(router), // Add Vue Router instance to the store }), persist: { @@ -24,17 +29,54 @@ export const useTokenStore = defineStore({ this.jwtToken = '' console.log(response.data) const decoded = jwtDecode(response.data) - console.log(jwtDecode(response.data)) - console.log(decoded.sub) - if(data != null && data !== '' && data !== undefined){ - this.jwtToken = data; - this.loggedInUser = username - this.password = password + if(data != null && data !== '' && data !== undefined && decoded !== undefined){ + const currentTime = Math.floor(Date.now() / 1000); + const timeToExpiration = decoded.exp - currentTime + if(timeToExpiration>0){ + this.jwtToken=data + this.loggedInUser=username + this.password=password + this.refreshTokenExpirationTimer(timeToExpiration); + } } - console.log(this.loggedInUser) + + } catch (err){ console.log(err) } - } + }, + async refreshToken(username: string, password:string) { + try { + // Make a request to the backend API to refresh the token + const response = await getJWTToken(username, password) + this.jwtToken = response.data + // Refresh the expiration timer with the new token's expiration time + const decoded = jwtDecode(this.jwtToken); + const currentTime = Math.floor(Date.now() / 1000); + const timeToExpiration = decoded.exp - currentTime; + this.refreshTokenExpirationTimer(timeToExpiration); + } catch (err) { + console.log(err); + } + }, + + refreshTokenExpirationTimer(timeToExpiration: number) { + // Clear existing timer if it exists + if (this.tokenExpirationTimer !== null) { + clearTimeout(this.tokenExpirationTimer); + } + // Start new timer to alert user when token is about to expire + this.tokenExpirationTimer = setTimeout(async () => { + if (confirm('Token is about to expire. Do you want to refresh?')) { + console.log(this.tokenExpirationTimer) + await this.refreshToken(this.loggedInUser, this.password); + // window.location.reload(); + } else { + await router.push("/") + console.log('Token expiration timer stopped.'); + } // Call token refresh method + }, timeToExpiration * 1000); // Convert seconds to milliseconds + }, + }, }); diff --git a/src/utils/httputils.ts b/src/utils/httputils.ts index 277bd1354f664fc717465ce9d3e360e4574d085d..9c5c85c07ca5fa77852811cf84cb7cfab0e634b0 100644 --- a/src/utils/httputils.ts +++ b/src/utils/httputils.ts @@ -340,3 +340,121 @@ export const getPicture = async (imageName: String, token: String) => { // Handle the error, display a message to the user, etc. } }; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +export const createUser = async (username: string, password: string, email: string) => { + try { + const data= { + username: username, + password: password, + email: email + } + + return await axios.post('http://localhost:8080/signup', data); + } catch (e) { + return "Failed to create user in backend" + } +} diff --git a/src/views/FrontPageView.vue b/src/views/FrontPageView.vue index 4e86b6fadcb7702901253d31f554ac7b6e53f99e..22efae7d11c82bf1e79bc5111020a3ce707cf1f3 100644 --- a/src/views/FrontPageView.vue +++ b/src/views/FrontPageView.vue @@ -10,6 +10,7 @@ onMounted(() => { tokenStore.jwtToken = '' tokenStore.loggedInUser='' tokenStore.password='' + tokenStore.tokenExpirationTimer=null }) </script> diff --git a/src/views/frontpage/SignUpView.vue b/src/views/frontpage/SignUpView.vue index 4ed434e33e72c9921d5eecc0bc6e29d1ab48aa3e..9ded0f165b8e1a9a737584cc8b25856d4b3e1b7b 100644 --- a/src/views/frontpage/SignUpView.vue +++ b/src/views/frontpage/SignUpView.vue @@ -1,7 +1,7 @@ <template> <div class="signup"> <UserSchema buttonText="Create Account" header-text="Create Account" :status="loginStatus" - :customLogin="customLoginFunction"></UserSchema> + :customLogin="handleLoginClick"></UserSchema> </div> </template> @@ -10,9 +10,11 @@ import { ref } from 'vue'; import UserSchema from '@/components/UserSchema.vue'; import router from '@/router' +import {useTokenStore} from "@/stores/token"; +import {createUser} from "@/utils/httputils"; let loginStatus = ref(''); - +const tokenStore = useTokenStore() const customLoginFunction = async (username: string, password: string, email: string) => { if (!username) { loginStatus.value = 'Please enter a username'; @@ -30,7 +32,7 @@ const customLoginFunction = async (username: string, password: string, email: st return; } - const apiUrl = 'http://localhost:8080/users/create'; + const apiUrl = 'http://localhost:8080/users/create'; try { const response = await fetch(apiUrl, { method: 'POST', @@ -51,4 +53,31 @@ const customLoginFunction = async (username: string, password: string, email: st loginStatus.value = err.message; } } + +async function handleLoginClick(username: string, password: string, email: string) { + if (!username) { + loginStatus.value = 'Please enter a username'; + return; + } + if (!password) { + loginStatus.value = 'Please enter a password'; + return; + } + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; + if (!emailRegex.test(email)) { + loginStatus.value = 'Please enter a valid email address'; + return; + } + try{ + await createUser(username, password, email) + await tokenStore.getTokenAndSaveInStore(username, password) + if(tokenStore.jwtToken) { + await router.push("/homepage/discovery") + } else { + loginStatus.value = "Failed to retrieve token!" + } + } catch (err: any) { + loginStatus.value = err + } +} </script> diff --git a/src/views/mainpage/CreateTrivio.vue b/src/views/mainpage/CreateTrivio.vue index b429352e7761fb0dc554558d0b59eac65498a026..77acbff502735ca2552ae078c7fb054ef48bfa02 100644 --- a/src/views/mainpage/CreateTrivio.vue +++ b/src/views/mainpage/CreateTrivio.vue @@ -3,6 +3,7 @@ import { ref, watch} from 'vue' import EditTrivioQuestion from '@/components/TrivioQuestion.vue' import { useTokenStore } from '@/stores/token' import {getPicture, postNewTrivio, uploadPicture} from "@/utils/httputils"; +import {useRouter} from "vue-router"; const tokenStore = useTokenStore(); // Reactive variables @@ -15,6 +16,7 @@ const questions = ref<Question[]>([]) const numberOfQuestions = ref<number[]>([]) const fileInput = ref(null); const uploadStatus = ref(''); +const router = useRouter() //default user = 1 const userID = 1 let uniqueComponentId = 0; @@ -89,6 +91,7 @@ const saveQuizToBackend = async () => { const token = tokenStore.jwtToken; if(!token){ console.log("unauthorized user") + alert("session ended or unauthorized user") } else{ const trivioData = { trivio: { @@ -115,6 +118,7 @@ const saveQuizToBackend = async () => { }; await postNewTrivio(token, trivioData); console.log('Quiz saved successfully!'); + await router.push("/homepage/mytrivios") } } catch (error) { console.error('Error saving quiz:', error); @@ -151,9 +155,7 @@ const handleFileUpload = async () => { } -// Function to handle click on the default image const handleDefaultImageClick = () => { - // Trigger file input click to select a new image const fileInput = document.getElementById('fileInput'); if (fileInput) { fileInput.click(); diff --git a/src/views/mainpage/EditTrivioView.vue b/src/views/mainpage/EditTrivioView.vue index 49a25342da598f2f1fb912c5466768f722be5d04..a0055e8fd223fb1dec8eeeff20339c8322c06a9b 100644 --- a/src/views/mainpage/EditTrivioView.vue +++ b/src/views/mainpage/EditTrivioView.vue @@ -4,7 +4,7 @@ import EditTrivioQuestion from '@/components/TrivioQuestion.vue' import { useRoute } from 'vue-router' import { onMounted, ref } from 'vue' import { useTokenStore } from '@/stores/token' -import { getTrivioById, editTrivio } from '@/utils/httputils' +import {getTrivioById, editTrivio, getPicture, uploadPicture} from '@/utils/httputils' const route = useRoute() const tokenStore = useTokenStore() @@ -20,29 +20,39 @@ const numberOfQuestions = ref<number[]>([]) const userID = ref(''); const trivioImage = ref('') let uniqueComponentId = 0; - +const trivioBlobUrl = ref("") const trivio = ref(); +const questionBlobs = ref(new Map()) +const questionBlobUrl = ref("") +const fileInput = ref(null); onMounted(async () => { try { if(!tokenStore.jwtToken){ console.log("unauthorized user") } else { const response = await getTrivioById(tokenStore.jwtToken, trivioId); + trivio.value = response.data title.value = response.data.title; description.value = response.data.description; difficulty.value = response.data.difficulty.toLowerCase(); category.value = response.data.category.toLowerCase(); visibility.value = response.data.visibility.toLowerCase(); - + trivioBlobUrl.value = await getPicture(response.data.multimedia_url, tokenStore.jwtToken) + console.log(trivioBlobUrl.value) for (let i = 0; i < response.data.questionList.length; i++) { const questionData = response.data.questionList[i]; + questionBlobUrl.value = await getPicture(questionData.media, tokenStore.jwtToken) + questionBlobs.value.set(questionData.media, questionBlobUrl.value); // Store blob URL in the map with trivio ID as key + console.log(questionBlobUrl.value) questions.value.push({ questionId: i, question: questionData.question, questionType: questionData.questionType, answers: questionData.answers, - tags: questionData.tags + tags: questionData.tags, + media: questionData.media, + blobUrl: questionBlobUrl.value }); numberOfQuestions.value.push(i); } @@ -61,7 +71,9 @@ const addQuestion = () => { question: '', questionType: '', answers: [], - tags: [] + tags: [], + media: '', + blobUrl: '' }) uniqueComponentId ++; console.log(uniqueComponentId) @@ -83,7 +95,8 @@ const saveQuestion = (questionData: Question) => { question: question.question, questionType: question.questionType, answers: question.answers, - tags: question.tags + tags: question.tags, + media: question.media }))) } }; @@ -101,13 +114,14 @@ const saveTrivio = async () =>{ difficulty: difficulty.value, visibility: visibility.value, category: category.value, - multimedia_url: "cat.png" + multimedia_url: trivioImage.value }, questionsWithAnswers: questions.value.map(question => ({ question: { question: question.question, questionType: question.questionType, - tags: question.tags.map(tag => tag) + tags: question.tags.map(tag => tag), + media: question.media, }, answers: question.answers.map(answer => ({ answer: answer.answer, @@ -116,6 +130,7 @@ const saveTrivio = async () =>{ })), userId: null }; + console.log(trivioData) console.log('data implemented'); await editTrivio(token, trivioData, trivioId); console.log('Quiz saved successfully!'); @@ -142,8 +157,56 @@ interface Question { questionType: string; answers: Answer[]; // Adjust the type of inputFields as needed tags: String[]; + media: string; + blobUrl: string; +} +const handleFileUpload = async () => { + if (fileInput && fileInput.value && fileInput.value.files && fileInput.value.files.length > 0) { + const file = fileInput.value.files[0]; + console.log(fileInput.value.files[0]) + try { + // Call the function to upload the picture + const uploadedImageUrl = await uploadPicture(file, tokenStore.jwtToken); + + const response = await getPicture(uploadedImageUrl, tokenStore.jwtToken) + trivioImage.value = uploadedImageUrl + trivioBlobUrl.value = response + console.log('Uploaded picture URL:', uploadedImageUrl); + console.log(uploadedImageUrl); + + console.log('Uploaded picture URL:', imageUrl); + + } catch (error) { + console.error('Error uploading picture:', error); + // Handle the error, display a message to the user, etc. + } + } else { + console.error('No file selected.'); + // Handle the case where no file is selected, display a message to the user, etc. + } } +const getBlobUrl = async (multimediaUrl: string) => { + if (!multimediaUrl) return ""; + try { + return await getPicture(multimediaUrl, tokenStore.jwtToken); + } catch (error) { + console.error("Error fetching blobUrl:", error); + return ""; + } +}; + +const fetchBlobUrls = async () => { + +}; +// Function to handle click on the default image +const handleDefaultImageClick = () => { + // Trigger file input click to select a new image + const fileInput = document.getElementById('fileInput'); + if (fileInput) { + fileInput.click(); + } +} </script> <template> @@ -184,8 +247,11 @@ interface Question { </div> <div class="right"> <div class="image-box"> - <h1>Add image</h1> - <img src="/src/components/icons/AddImage.png" alt="Image" width="50px" height="50px"> + <label> + <input type="file" style="display: none" ref="fileInput" accept="image/png, image/jpeg" @change="handleFileUpload"> + <img v-if="trivioBlobUrl" :src="trivioBlobUrl" alt="Uploaded Image" width="150px" height="150px"> + <img v-else src="/src/components/icons/AddImage.png" alt="Add Image" width="50px" height="50px" @click="handleDefaultImageClick"> + </label> </div> </div> </div> @@ -197,6 +263,7 @@ interface Question { :index="index + 1" :question-id="questionId" :question="questions[questionId]" + :blob-url="questionBlobs.get(questions[questionId].media)" @delete-question="deleteQuestion(questionId)" @save-question="saveQuestion" ></EditTrivioQuestion> diff --git a/src/views/mainpage/HistoryView.vue b/src/views/mainpage/HistoryView.vue index c21419ecb0538848d5e9692f7cad9d72b08d49f5..bc2bb0639e4db0e82c4d92bfaefcf287248cbf1e 100644 --- a/src/views/mainpage/HistoryView.vue +++ b/src/views/mainpage/HistoryView.vue @@ -3,15 +3,37 @@ import {onMounted, ref} from "vue"; import axios from "axios"; import HistoryBox from "@/components/HistoryBox.vue"; import TrivioCard from "@/components/TrivioCard.vue"; -import {getHistoryByUser} from "@/utils/httputils"; +import {getHistoryByUser, getPicture} from "@/utils/httputils"; import {useTokenStore} from "@/stores/token"; const results = ref("") const tokenStore = useTokenStore() +const trivioBlobUrls = ref(new Map()); // Store blob URLs for each trivio +const getBlobUrl = async (multimediaUrl: string) => { + if (!multimediaUrl) return ""; + try { + return await getPicture(multimediaUrl, tokenStore.jwtToken); + } catch (error) { + console.error("Error fetching blobUrl:", error); + return ""; + } +}; + +const fetchBlobUrls = async () => { + for (const result of results.value) { + console.log(result) + const trivio = result.trivio + console.log(trivio) + const blobUrl = await getBlobUrl(trivio.multimedia_url); + trivioBlobUrls.value.set(trivio.id, blobUrl); // Store blob URL in the map with trivio ID as key + } +}; + onMounted(async () => { try { const response = await getHistoryByUser(tokenStore.jwtToken); //for now just hardcode userId results.value = response.data; - console.log(results.value) + //console.log(results.value[0].trivio) + await fetchBlobUrls() } catch (error) { console.error('Error fetching quiz data:', error); } @@ -31,6 +53,8 @@ onMounted(async () => { :difficulty="result.trivio?.difficulty" :score="result.score" :username="result.user?.username" + :image="trivioBlobUrls.get(result.trivio?.id)" + :date="result.postedDate" ></history-box> </div> </div>