diff --git a/README.md b/README.md index 4b4ef0329295cb8daf8b51a4a3dff51ebe19441a..53ff42d83e39d6ac507fd62528dee6ae81f372d9 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,93 @@ -# trivio +# frontend_fullstack -This template should help get you started developing with Vue 3 in Vite. -## Recommended IDE Setup -[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). +## Getting started -## Type Support for `.vue` Imports in TS +To make it easy for you to get started with GitLab, here's a list of recommended next steps. -TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! -## Customize configuration +## Add your files -See [Vite Configuration Reference](https://vitejs.dev/config/). +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: -## Project Setup - -```sh -npm install +``` +cd existing_repo +git remote add origin https://gitlab.stud.idi.ntnu.no/team_fullastack/frontend_fullstack.git +git branch -M main +git push -uf origin main ``` -### Compile and Hot-Reload for Development +## Integrate with your tools -```sh -npm run dev -``` +- [ ] [Set up project integrations](https://gitlab.stud.idi.ntnu.no/team_fullastack/frontend_fullstack/-/settings/integrations) -### Type-Check, Compile and Minify for Production +## Collaborate with your team -```sh -npm run build -``` +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) -### Run Unit Tests with [Vitest](https://vitest.dev/) +## Test and Deploy -```sh -npm run test:unit -``` +Use the built-in continuous integration in GitLab. -### Run End-to-End Tests with [Cypress](https://www.cypress.io/) +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) -```sh -npm run test:e2e:dev -``` +*** -This runs the end-to-end tests against the Vite development server. -It is much faster than the production build. +# Editing this README -But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. -```sh -npm run build -npm run test:e2e -``` +## Suggestions for a good README -### Lint with [ESLint](https://eslint.org/) +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. -```sh -npm run lint -``` +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/src/components/QuestionBox.vue b/src/components/QuestionBox.vue index 1f2fb8799195198c045ad7f09740f971d768514f..105edd05b43286d695bf1c67ff5d3f68c162c554 100644 --- a/src/components/QuestionBox.vue +++ b/src/components/QuestionBox.vue @@ -20,7 +20,8 @@ const props = defineProps({ </div> </div> </div> - <img src="/src/components/icons/cat.png" alt="Question picture"> + <img v-if="image":src="image" alt="Question picture"> + <img v-else src="/src/assets/trivio.svg" alt="no picture"> </div> </div> </template> diff --git a/src/components/TrivioCard.vue b/src/components/TrivioCard.vue index 1d696e7fa5f7dc0348efa3672f4f7ee1dd214602..f1555034093c4979d134bb113cc0ac3c7d0b269a 100644 --- a/src/components/TrivioCard.vue +++ b/src/components/TrivioCard.vue @@ -5,14 +5,17 @@ const props = defineProps({ category: String, numberOfQuestion: Number, username: String, - difficulty: String + difficulty: String, + media: String, }); </script> <template> <div class="box" @mouseover="isHover = true" @mouseleave="isHover = false"> <div class="quiz-img"> - <img src="/src/components/icons/cat.png" alt="Quiz picture"> + <img v-if="media" :src="media" alt="Quiz picture"> + <img v-else src="/src/assets/trivio.svg" alt="N/A image"> + </div> <div class="quiz-info"> <div class="title">{{ title }}</div> diff --git a/src/components/TrivioQuestion.vue b/src/components/TrivioQuestion.vue index 5b1a6856037bf0d8a4877a73afe43ac0908440d6..955800eae989d6d38d9d23a11d59ec23cf645c05 100644 --- a/src/components/TrivioQuestion.vue +++ b/src/components/TrivioQuestion.vue @@ -1,16 +1,18 @@ <script setup lang="ts"> import { ref, defineProps, defineEmits, watch} from 'vue' +import {getPicture, uploadPicture} from "@/utils/httputils"; +import {useTokenStore} from "@/stores/token"; const props = defineProps<{ index:number; questionId:number; question: questionData; }>(); - +const tokenStore = useTokenStore() const emit = defineEmits(['delete-question', 'save-question']); const questionType = ref<string>(props.question ? props.question.questionType || 'multiple' : 'multiple'); const question = ref<string>(props.question ? props.question.question : ''); - +const fileInput = ref(null) const answers = ref<Answer[]>(questionType.value === 'multiple' ? [ { answer: props.question?.answers?.[0]?.answer || '', correct: props.question?.answers?.[0]?.correct || false }, @@ -25,6 +27,9 @@ const answers = ref<Answer[]>(questionType.value === 'multiple' ); const tags = ref<string[]>(props.question && props.question.tags ? [...props.question.tags] : []); +const media = ref( props.question? props.question.media || props.question.media: "") +const blobUrl = ref("") + interface Answer { answer: string; correct: boolean; @@ -36,6 +41,7 @@ interface questionData{ questionType: string, answers: Answer[], tags: string[], + media: string } const addTag = () => { @@ -46,6 +52,9 @@ const deleteTag = (index: number) => { tags.value.splice(index, 1); }; +const addMedia = () => { + media.value +} const deleteThisQuestion = () => { emit('delete-question', props.questionId); }; @@ -63,13 +72,39 @@ const preventSpace = (event: KeyboardEvent) => { event.preventDefault(); } }; + +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 uploadedImage = await uploadPicture(file, tokenStore.jwtToken); + const uploadedImageUrl = await getPicture(uploadedImage, tokenStore.jwtToken) + media.value = uploadedImage; + blobUrl.value = uploadedImageUrl + console.log('Uploaded picture URL:', uploadedImageUrl); + console.log(uploadedImageUrl); + + console.log('Uploaded picture URL:', media); + + } 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 saveQuestionData = () => { const questionData = { questionId: props.questionId, question: question.value, questionType: questionType.value, answers: answers.value, - tags: tags.value + tags: tags.value, + media: media.value }; emit('save-question', questionData); }; @@ -93,7 +128,7 @@ watch(questionType, (newQuestionType) => { }); watch(answers, saveQuestionData, { deep: true }); watch(tags, saveQuestionData, {deep: true}); - +watch(media, saveQuestionData, {deep: true}) </script> <template> <div class="trivio-question"> @@ -137,8 +172,11 @@ watch(tags, saveQuestionData, {deep: true}); </select> </div> <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="media" :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> </div> </div> diff --git a/src/stores/token.ts b/src/stores/token.ts index 0130760daab6440e7c558a7c3ce9c5103f428a82..cfb627e9aa591b2265cfdbea115a31a0d2eff34a 100644 --- a/src/stores/token.ts +++ b/src/stores/token.ts @@ -8,6 +8,7 @@ export const useTokenStore = defineStore({ state: () => ({ jwtToken: "", loggedInUser: "", + password: "" }), persist: { @@ -28,6 +29,7 @@ export const useTokenStore = defineStore({ if(data != null && data !== '' && data !== undefined){ this.jwtToken = data; this.loggedInUser = username + this.password = password } console.log(this.loggedInUser) } catch (err){ diff --git a/src/views/FrontPageView.vue b/src/views/FrontPageView.vue index bde1f5828606326de747853445d8bde4e09e5f83..4e86b6fadcb7702901253d31f554ac7b6e53f99e 100644 --- a/src/views/FrontPageView.vue +++ b/src/views/FrontPageView.vue @@ -2,6 +2,15 @@ import HelloWorld from '@/components/HelloWorld.vue' import { RouterLink } from 'vue-router' +import {useTokenStore} from "@/stores/token"; +import {onMounted} from "vue"; +const tokenStore = useTokenStore() + +onMounted(() => { + tokenStore.jwtToken = '' + tokenStore.loggedInUser='' + tokenStore.password='' +}) </script> <template> @@ -98,6 +107,6 @@ nav a:first-of-type { } @media screen { - + } -</style> \ No newline at end of file +</style> diff --git a/src/views/mainpage/ContactView.vue b/src/views/mainpage/ContactView.vue index b1b29d7d0a61404fd798d2168e82a8a49ef4ed35..70d8d1b13c5c366c9c8aee5be03c167a1a5432cf 100644 --- a/src/views/mainpage/ContactView.vue +++ b/src/views/mainpage/ContactView.vue @@ -1,15 +1,17 @@ <script setup lang="ts"> import { ref, reactive } from 'vue'; +import axios from "axios"; +import {useTokenStore} from "@/stores/token"; +import {postMessage} from "@/utils/httputils"; +const tokenStore = useTokenStore() const form = reactive({ - name: '', // Will be replaced by username from backend - email: '', // Will be replaced by email from backend - message: '', - response: '' + name: "", + email: "", + message: "", + response: "" }); -const dummyUsername = 'John Doe'; // Dummy username until backend is implemented -const dummyEmail = 'john.doe@example.com'; // Dummy email until backend is implemented const sendMessage = async () => { if (!form.message.trim()) { @@ -18,31 +20,22 @@ const sendMessage = async () => { } try { - const response = await fetch('http://localhost:8080/messages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(form) - }); - - const responseData = await response.json(); - - if (response.ok) { - form.message = ''; - form.response = responseData.message; - } else { - form.response = responseData.message; + const data = { + senderEmail: form.email, + content: form.message, + senderName: form.name } + const response = await postMessage(tokenStore.jwtToken, data) + console.log(form) + form.response = response.data.message } catch (error) { console.error('Error sending message:', error); - form.response = 'An error occurred while sending the message'; + form.value.response = 'An error occurred while sending the message'; } }; // Use dummy values until backend provides actual username and email -form.name = dummyUsername; -form.email = dummyEmail; + </script> <template> @@ -51,11 +44,11 @@ form.email = dummyEmail; <h1>Contact us</h1> <div class="row"> <label class="required" for="name">Name:</label><br> - <input id="name" class="input" :value="form.name" disabled><br> + <input id="name" class="input" v-model="form.name" ><br> </div> <div class="row"> <label class="required" for="email">Email:</label><br> - <input id="email" class="input" :value="form.email" disabled><br> + <input id="email" class="input" v-model="form.email"><br> </div> <div class="row"> <label class="required" for="message">Message:</label><br> @@ -138,7 +131,7 @@ input[type=submit]:hover { } textarea { - resize: none; + resize: none; } .response { @@ -161,4 +154,4 @@ textarea { color: #636363; text-align: center; } -</style> \ No newline at end of file +</style> diff --git a/src/views/mainpage/CreateTrivio.vue b/src/views/mainpage/CreateTrivio.vue index ebae3bec2652b4a11f51b00133d5389430a588c4..b429352e7761fb0dc554558d0b59eac65498a026 100644 --- a/src/views/mainpage/CreateTrivio.vue +++ b/src/views/mainpage/CreateTrivio.vue @@ -2,7 +2,7 @@ import { ref, watch} from 'vue' import EditTrivioQuestion from '@/components/TrivioQuestion.vue' import { useTokenStore } from '@/stores/token' -import {postNewTrivio} from "@/utils/httputils"; +import {getPicture, postNewTrivio, uploadPicture} from "@/utils/httputils"; const tokenStore = useTokenStore(); // Reactive variables @@ -13,6 +13,8 @@ const category = ref<string>('random') const visibility = ref<string>('private') const questions = ref<Question[]>([]) const numberOfQuestions = ref<number[]>([]) +const fileInput = ref(null); +const uploadStatus = ref(''); //default user = 1 const userID = 1 let uniqueComponentId = 0; @@ -28,8 +30,12 @@ interface Question { questionType: string; answers: Answer[]; // Adjust the type of inputFields as needed tags: String[]; + media: string; } +const imageUrl = ref<string>(''); // Initially empty +const defaultImageUrl = '/src/components/icons/AddImage.png'; // Change this to your default image path +const imageBlobUrl = ref<string>(''); // Methods const addQuestion = () => { const newQuestionId = uniqueComponentId @@ -39,7 +45,8 @@ const addQuestion = () => { question: '', questionType: '', answers: [], - tags: [] + tags: [], + media: '', }) uniqueComponentId ++; console.log(uniqueComponentId) @@ -62,7 +69,8 @@ const saveQuestion = (questionData: Question) => { question: question.question, questionType: question.questionType, answers: question.answers, - tags: question.tags + tags: question.tags, + media: question.media }))) } }; @@ -89,13 +97,14 @@ const saveQuizToBackend = async () => { difficulty: difficulty.value, visibility: visibility.value, category: category.value, - multimedia_url: "cat.png" + multimedia_url: imageUrl.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, @@ -113,6 +122,43 @@ const saveQuizToBackend = async () => { } }; + + +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) + imageUrl.value = uploadedImageUrl + imageBlobUrl.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. + } +} + + +// 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> @@ -154,7 +200,11 @@ const saveQuizToBackend = async () => { <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="imageUrl" :src="imageBlobUrl" 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> @@ -318,4 +368,4 @@ const saveQuizToBackend = async () => { } -</style> \ No newline at end of file +</style> diff --git a/src/views/mainpage/DiscoveryView.vue b/src/views/mainpage/DiscoveryView.vue index 86600176ee4af2e906ff9572c027b5a256aa7b98..6c556c6e6bbcc650cbfb3413145783b818d72954 100644 --- a/src/views/mainpage/DiscoveryView.vue +++ b/src/views/mainpage/DiscoveryView.vue @@ -3,13 +3,13 @@ import { ref, onMounted, watch } from 'vue' import TrivioCard from "@/components/TrivioCard.vue"; import router from "@/router"; -import { getPublicTriviosFromOtherUsers } from '@/utils/httputils' +import {getPicture, getPublicTriviosFromOtherUsers} from '@/utils/httputils' import { useTokenStore } from '@/stores/token' const trivios = ref(null); const triviosToDisplay= ref(null) const tokenStore = useTokenStore() - +const triviosBlobUrls = ref(new Map()) interface Tag{ tag: String; } @@ -22,6 +22,7 @@ onMounted(async () => { const response = await getPublicTriviosFromOtherUsers(tokenStore.jwtToken); trivios.value = response.data console.log(trivios.value) + await fetchBlobUrls() filterMyTrivios(); } }); @@ -63,7 +64,22 @@ const filterMyTrivios = () => { return categoryMatch && difficultyMatch && tagsMatch; }); }; +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 trivio of trivios.value) { + const blobUrl = await getBlobUrl(trivio.multimedia_url); + triviosBlobUrls.value.set(trivio.id, blobUrl); // Store blob URL in the map with trivio ID as key + } +}; watch(tags, (newTags, oldTags) => { filterMyTrivios(); }, { deep: true }); @@ -112,6 +128,7 @@ watch(difficulty, filterMyTrivios); :number-of-question="trivio.questionList.length" :id="trivio.id" :difficulty="trivio.difficulty" + :media="triviosBlobUrls.get(trivio.id)" @click="handleCardClick(trivio.id)" ></TrivioCard> </div> diff --git a/src/views/mainpage/EditTrivioView.vue b/src/views/mainpage/EditTrivioView.vue index 9e6aa5adaf51c76ce2e62658c648c3238bf03e6b..49a25342da598f2f1fb912c5466768f722be5d04 100644 --- a/src/views/mainpage/EditTrivioView.vue +++ b/src/views/mainpage/EditTrivioView.vue @@ -18,6 +18,7 @@ const visibility = ref<string>('') const questions = ref<Question[]>([]) const numberOfQuestions = ref<number[]>([]) const userID = ref(''); +const trivioImage = ref('') let uniqueComponentId = 0; const trivio = ref(); @@ -348,4 +349,4 @@ interface Question { .add-question{ border-radius: 50px; } -</style> \ No newline at end of file +</style> diff --git a/src/views/mainpage/MyTriviosView.vue b/src/views/mainpage/MyTriviosView.vue index 85e95590d164630d6d41e811744be6705afd525d..698e1802fde3c506579ec89e19c523fa482c1c75 100644 --- a/src/views/mainpage/MyTriviosView.vue +++ b/src/views/mainpage/MyTriviosView.vue @@ -1,19 +1,27 @@ <script setup lang="ts"> -import { ref, onMounted, watch } from 'vue' +import {onMounted, ref, watch} from 'vue' import TrivioCard from "@/components/TrivioCard.vue"; import router from "@/router"; -import {getTriviosByUser} from "@/utils/httputils"; +import {getPicture, getTriviosByUser} from "@/utils/httputils"; import {useTokenStore} from "@/stores/token"; const trivios = ref(null); const triviosToDisplay= ref(null) const tokenStore = useTokenStore() +const trivioBlobUrls = ref(new Map()); // Store blob URLs for each trivio interface Tag{ tag: String; } + +const image = ref({ + blobUrl: "", + imageUrl: "" +}); + + onMounted(async () => { if(!tokenStore.jwtToken){ console.log("unauthorized user") @@ -23,6 +31,7 @@ onMounted(async () => { const response = await getTriviosByUser(tokenStore.jwtToken); trivios.value = response.data console.log(trivios.value) + await fetchBlobUrls() filterMyTrivios(); } }); @@ -42,6 +51,23 @@ const deleteTag = (index: number) => { tags.value.splice(index, 1); }; +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 trivio of trivios.value) { + const blobUrl = await getBlobUrl(trivio.multimedia_url); + trivioBlobUrls.value.set(trivio.id, blobUrl); // Store blob URL in the map with trivio ID as key + } +}; + const filterMyTrivios = () => { // Apply filters based on category, difficulty, and tags triviosToDisplay.value = trivios.value.filter((trivio: any) => { @@ -113,6 +139,7 @@ watch(difficulty, filterMyTrivios); :number-of-question="trivio.questionList.length" :id="trivio.id" :difficulty="trivio.difficulty" + :media="trivioBlobUrls.get(trivio.id)" @click="handleCardClick(trivio.id)" ></TrivioCard> </div> diff --git a/src/views/mainpage/PlayView.vue b/src/views/mainpage/PlayView.vue index 5ea7cb296802e4b20703cb41cf54f054256cdee9..36c5a94c0e5bae49702f7a03d1e384d414175b95 100644 --- a/src/views/mainpage/PlayView.vue +++ b/src/views/mainpage/PlayView.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> -import { computed, onMounted, ref } from 'vue'; -import { useRoute } from "vue-router"; +import {computed, onMounted, ref} from 'vue'; +import {useRoute} from "vue-router"; import axios from "axios"; -import {getTrivioById, getUserInfo} from "@/utils/httputils"; +import {getPicture, getTrivioById, getUserInfo} from "@/utils/httputils"; import {useTokenStore} from "@/stores/token"; + const route = useRoute() const trivio = ref(""); const trivioId = route.params.id; @@ -14,6 +15,9 @@ const score = ref(0) const quizDone = ref(false); const tokenStore = useTokenStore() const user = ref("") +const imageUrl = ref("") +const blobUrls = ref(new Map()); + const currentQuestion = computed(() => { if (trivio.value && trivio.value.questionList && trivio.value.questionList.length > 0) { return trivio.value.questionList[currentQuestionIndex.value].question; @@ -22,6 +26,16 @@ const currentQuestion = computed(() => { } }); +const currentImage = computed(() => { + if (trivio.value && trivio.value.questionList && trivio.value.questionList.length > 0) { + return trivio.value.questionList[currentQuestionIndex.value].media; + } else { + return ''; + } +}); + + + const currentAnswers = computed(() => { if (trivio.value && trivio.value.questionList && trivio.value.questionList.length > 0) { return trivio.value.questionList[currentQuestionIndex.value].answers; @@ -36,8 +50,14 @@ const isLastQuestion = computed(() => { const abcdef = () => { + + console.log(currentQuestionIndex.value) console.log("currentQuestionIndex:", currentQuestionIndex.value); console.log(trivio.value.questionList.length) + console.log(trivio.value.questionList[currentQuestionIndex.value].media) + console.log(blobUrls.value.get(trivio.value.questionList[currentQuestionIndex.value].media)) + console.log(currentImage.value) + console.log(blobUrls.value.get(currentImage.value)) } const handleAnswerClick = (answer) => { if (!isAnswerChosen.value) { @@ -53,14 +73,8 @@ const handleAnswerClick = (answer) => { } }; const goToNextQuestion = () => { - console.log(currentQuestionIndex.value) - - // if (currentQuestionIndex.value + 1 >= trivio.value.questionList.length) { - // console.log('End of quiz'); - // quizDone.value = true; - // return - // } - + imageUrl.value = getPicture(currentImage.value, tokenStore.jwtToken) + console.log(currentImage.value) currentQuestionIndex.value++; isAnswerChosen.value = false // Reset selectedAnswer to null for the next question @@ -89,6 +103,22 @@ const submitQuiz = async () => { // Handle error, e.g., showing an error message to the user } } +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 question of trivio.value.questionList) { + const blobUrl = await getBlobUrl(question.media); + blobUrls.value.set(question.media, blobUrl); // Store blob URL in the map with trivio ID as key + } +}; onMounted(async () => { try { const response1 = await getUserInfo(tokenStore.jwtToken) @@ -96,7 +126,8 @@ onMounted(async () => { console.log(user.value) const response = await getTrivioById(tokenStore.jwtToken, trivioId); trivio.value = response.data; - console.log(trivio.value) + await fetchBlobUrls() + // Optionally, you can also update the currentImage value here if needed } catch (error) { console.error('Error fetching quiz data:', error); } @@ -126,7 +157,8 @@ onMounted(async () => { <div class="content-container"> <div class="center-container"> <div class="question-text">{{ currentQuestion }}</div> - <img src="../../assets/trivio.svg" @click="abcdef" alt="Question Image"> + <img v-if="currentImage" :src="blobUrls.get(currentImage)" @click="abcdef" alt="Question Image"> + <img v-else src="/src/assets/trivio.svg" </div> <!-- Answer grid --> diff --git a/src/views/mainpage/ProfileView.vue b/src/views/mainpage/ProfileView.vue index cc42cfc5c1c863b56972cf9114e7ccb276cfe89c..1f74ac2345574138d6af37ed14225101353110da 100644 --- a/src/views/mainpage/ProfileView.vue +++ b/src/views/mainpage/ProfileView.vue @@ -1,59 +1,129 @@ <script setup lang="ts"> -import { ref, reactive, watch } from 'vue'; - -interface UserProfile { - username: string; - email: string; -} - -interface UserPassword { - oldPassword: string; - newPassword: string; -} - -function initialUserProfileState(): UserProfile { - return { - username: 'TestUser1', - email: 'test@example.com', - }; +import {ref, reactive, watch, onMounted} from 'vue'; +import {getTriviosByUser, getUserInfo1, putPasswordInfo, updateUserInfo} from "@/utils/httputils"; +import {useTokenStore} from "@/stores/token"; +const tokenStore = useTokenStore() +const user = ref({ + username: "", + email: "" +}); +const userProfileChanged = ref(false); +const userPasswordChanged = ref(false); +const newPassword = ref("") +const oldPassword = ref("") +let initialUserProfileState = { + username: "", + email: "" } - -function initialUserPasswordState(): UserPassword { - return { - oldPassword: '', - newPassword: '', - }; +let initialPasswordState = { + oldPassword: "", + newPassword: "" } +onMounted(async () => { + if(!tokenStore.jwtToken){ + console.log("unauthorized user") + } else { + + const response = await getUserInfo1(tokenStore.jwtToken); + user.value = response.data + console.log(user.value) + initialUserProfileState = { ...response.data }; + } +}); -const userProfile = reactive(initialUserProfileState()); -const userPassword = reactive(initialUserPasswordState()); +watch(() => user.value.username, (newVal, oldVal) => { + userProfileChanged.value = newVal !== initialUserProfileState.username || user.value.email !== initialUserProfileState.email; +}); -const userProfileChanged = ref(false); -const userPasswordChanged = ref(false); +watch(() => user.value.email, (newVal, oldVal) => { + userProfileChanged.value = newVal !== initialUserProfileState.email || user.value.username !== initialUserProfileState.username; +}); -// Track changes in user profile -watch(userProfile, (newVal, oldVal) => { - userProfileChanged.value = JSON.stringify(newVal) !== JSON.stringify(initialUserProfileState()); +watch(newPassword, (newVal, oldVal) => { + userPasswordChanged.value = newVal !== initialPasswordState.oldPassword || oldPassword.value !== initialPasswordState.newPassword; }); -// Track changes in user password -watch(userPassword, (newVal, oldVal) => { - userPasswordChanged.value = JSON.stringify(newVal) !== JSON.stringify(initialUserPasswordState()); +watch(oldPassword, (newVal, oldVal) => { + userPasswordChanged.value = newVal !== initialPasswordState.oldPassword || oldPassword.value !== initialPasswordState.newPassword; }); -const saveProfile = () => { - alert('Profile saved: ' + JSON.stringify(userProfile)); - userProfileChanged.value = false; +const saveProfile = async (token, username, email) => { + try { + // Call updateUserInfo function with provided parameters + const response = await updateUserInfo(token, username, email); + console.log("User profile saved successfully"); + console.log(response.data) + tokenStore.loggedInUser = username + } catch (error) { + console.error('Error saving user profile:', error); + } }; - -const changePassword = () => { - if (userPassword.newPassword !== userPassword.oldPassword) { - alert('Passwords do not match!'); - return; +const checkIfUserIsAuthorized = (oldPassword: string) => { + return oldPassword === tokenStore.password +} +const updatePassword = async (token: any,newPassword: any,oldPassword: any) => { + try { + console.log(checkIfUserIsAuthorized(oldPassword)) + if(checkIfUserIsAuthorized(oldPassword)){ + const response = await putPasswordInfo(token, newPassword); + console.log(response) + tokenStore.password = newPassword + } else { + alert("Current password is wrong!") + } + } catch (error) { + console.error('Error saving user profile:', error); } - alert('Password changed successfully'); - userPasswordChanged.value = false; }; +// interface UserProfile { +// username: string; +// email: string; +// } +// +// interface UserPassword { +// oldPassword: string; +// newPassword: string; +// } + +// function initialUserProfileState(): UserProfile { +// return { +// username: 'TestUser1', +// email: 'test@example.com', +// }; +// } +// +// function initialUserPasswordState(): UserPassword { +// return { +// oldPassword: '', +// newPassword: '', +// }; +// } +// +// const userProfile = reactive(initialUserProfileState()); +// const userPassword = reactive(initialUserPasswordState()); +// + +// +// // Track changes in user profile + +// +// // Track changes in user password + +// +// const saveProfile = () => { +// alert('Profile saved: ' + JSON.stringify(userProfile)); +// userProfileChanged.value = false; +// }; +// +// const changePassword = () => { +// if (userPassword.newPassword !== userPassword.oldPassword) { +// alert('Passwords do not match!'); +// return; +// } +// alert('Password changed successfully'); +// userPasswordChanged.value = false; +// }; + </script> <template> @@ -64,7 +134,7 @@ const changePassword = () => { <div class="user-information-section"> <div class="user-info-header"> <h3 class="section-heading">User information</h3> - <button class="save-button" :disabled="!userProfileChanged" @click="saveProfile">SAVE</button> + <button class="save-button" :disabled="!userProfileChanged" @click="saveProfile(tokenStore.jwtToken, user.username, user.email)">SAVE</button> </div> <div class="user-info"> <div class="profile-image"> @@ -72,9 +142,9 @@ const changePassword = () => { </div> <div class="info-fields"> <label for="username">Username:</label> - <input type="text" id="username" v-model="userProfile.username" /> + <input type="text" id="username" v-model="user.username" /> <label for="email">Email:</label> - <input type="email" id="email" v-model="userProfile.email" /> + <input type="email" id="email" v-model="user.email" /> </div> </div> </div> @@ -83,13 +153,13 @@ const changePassword = () => { <div class="password-change-section"> <div class="password-header"> <h3 class="section-heading">Change password</h3> - <button class="save-button" :disabled="!userPasswordChanged" @click="changePassword">SAVE</button> + <button class="save-button" :disabled="!userPasswordChanged" @click="updatePassword(tokenStore.jwtToken, newPassword, oldPassword)">SAVE</button> </div> <div class="password-fields"> - <label for="old-password">Old password:</label> - <input type="password" id="old-password" v-model="userPassword.oldPassword" /> - <label for="new-password">New password:</label> - <input type="password" id="new-password" v-model="userPassword.newPassword" /> + <label for="old-password">Current Password:</label> + <input type="password" id="old-password" v-model="oldPassword" /> + <label for="new-password">New Password:</label> + <input type="password" id="new-password" v-model="newPassword" /> </div> </div> </div> @@ -145,16 +215,16 @@ h3 { } .user-info { - display: flex; + display: flex; align-items: flex-start; - gap: 1em; + gap: 1em; } .password-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.5em; + margin-bottom: 0.5em; } .password-fields { @@ -164,9 +234,9 @@ h3 { } .profile-image { - width: 150px; + width: 150px; height: 150px; - margin-top: 0.6em; + margin-top: 0.6em; } .profile-image img { @@ -178,12 +248,12 @@ h3 { .info-fields { display: flex; - flex-direction: column; + flex-direction: column; width: 100%; } .field { - margin-bottom: 1em; + margin-bottom: 1em; } label { @@ -211,7 +281,7 @@ input[type="password"] { } .save-button:disabled { - background: #cccccc; + background: #cccccc; color: #666666; cursor: not-allowed; } diff --git a/src/views/mainpage/StartView.vue b/src/views/mainpage/StartView.vue index 3f6eb8276f63f8abb7e24cdb0429d95b4ee2242a..1604eee7c72d46ddd50fd38ce4d674bc2998bd42 100644 --- a/src/views/mainpage/StartView.vue +++ b/src/views/mainpage/StartView.vue @@ -71,6 +71,8 @@ onMounted(async () => { const response = await getTrivioById(tokenStore.jwtToken, trivioId) console.log(response) trivio.value = response.data + blobUrl.value = await getPicture(trivio.value.multimedia_url, tokenStore.jwtToken) + await fetchBlobUrls() } catch (error) { console.error('Error fetching quiz data:', error); } @@ -81,7 +83,8 @@ onMounted(async () => { <div class="container"> <div class="left-container"> <div class="quiz-image"> - <img src="/src/components/icons/cat.png" alt="Quiz Image"> + <img v-if="blobUrl" :src="blobUrl" alt="Quiz Image"> + <img v-else src="/src/assets/trivio.svg" alt="N/A image"> </div> <div class="quiz-info"> <h1 class="TrivioTitle">{{ trivio.title }}</h1> @@ -148,6 +151,7 @@ onMounted(async () => { :question="question.question" :tags="question.tags" :questionNumber="index + 1" + :image="questionBlobUrls.get(question.question)" /> </div> </div>