mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Formbricks (#157)
add quest feedback and bug report form using Formbricks
This commit is contained in:
11 files changed
+2642
-1293
No files matched your search
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formbricks/js": "^2.1.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import CompleteAnimation from '../../img/complete-animation.json'
|
|||||||
const showModal = ref(true) // Controls whether the modal is visible or not
|
const showModal = ref(true) // Controls whether the modal is visible or not
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const defineComponent = () => ({
|
const defineComponent = () => ({
|
||||||
components: {
|
components: {
|
||||||
Vue3Lottie,
|
Vue3Lottie,
|
||||||
@@ -22,6 +24,7 @@ const defineComponent = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const backtoMap = async () => {
|
const backtoMap = async () => {
|
||||||
try {
|
try {
|
||||||
router.push({ name: 'map' })
|
router.push({ name: 'map' })
|
||||||
@@ -67,6 +70,21 @@ setTimeout(() => {
|
|||||||
>回到地圖</a
|
>回到地圖</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="position: relative; height: 100vh; overflow: auto">
|
||||||
|
<iframe
|
||||||
|
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
|
||||||
|
frameborder="0"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import useSession from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import { surveyUrl } from '../../config'
|
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const sessionStore = useSession()
|
const sessionStore = useSession()
|
||||||
@@ -21,31 +21,39 @@ const continuePlay = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section class="mt-2 flex flex-row gap-3 justify-center">
|
||||||
class="h-fit w-fit px-1 py-1 grid grid-cols-4 gap-2 place-content-between"
|
<button
|
||||||
>
|
id="survey"
|
||||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
class="bg-bg-secondary rounded-lg p-2"
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
@click="openQuestSurvey"
|
||||||
<font-awesome-icon
|
>
|
||||||
:icon="['fas', 'file-invoice']"
|
<font-awesome-icon
|
||||||
class="text-text-primary px-1"
|
:icon="['fas', 'file-invoice']"
|
||||||
/>
|
class="text-text-primary px-1"
|
||||||
<p class="text-text-primary inline ml-1 md:flex">問卷</p>
|
/>
|
||||||
</a>
|
<p class="text-text-primary inline md:flex">問卷</p>
|
||||||
</button>
|
</button>
|
||||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
<button
|
||||||
|
title="Bug Report"
|
||||||
|
@click="openBugReport"
|
||||||
|
class="bg-bg-secondary rounded-lg p-2"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
|
<p class="text-text-primary inline ml-1 md:flex">回報</p>
|
||||||
|
</button>
|
||||||
|
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['far', 'circle-question']"
|
:icon="['far', 'circle-question']"
|
||||||
class="text-text-disabled px-1"
|
class="text-text-disabled px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||||
</button>
|
</button> -->
|
||||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'arrow-rotate-left']"
|
:icon="['fas', 'arrow-rotate-left']"
|
||||||
class="text-text-primary px-1"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1 hidden md:flex">重來</p>
|
<p class="text-text-primary inline ml-1 md:flex">重來</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="continue"
|
id="continue"
|
||||||
@@ -56,7 +64,7 @@ const continuePlay = async () => {
|
|||||||
:icon="['far', 'circle-right']"
|
:icon="['far', 'circle-right']"
|
||||||
class="text-text-primary px-1"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1 hidden md:flex">繼續</p>
|
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,7 +3,8 @@ import { onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { logout } from '../../utils/auth'
|
import { logout } from '../../utils/auth'
|
||||||
import { bugReportUrl, surveyUrl } from '../../config'
|
import { surveyUrl } from '../../config'
|
||||||
|
import { openBugReport } from '../../utils/formbricks'
|
||||||
import useSession from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import useUserProfile from '../../store/userProfile'
|
import useUserProfile from '../../store/userProfile'
|
||||||
|
|
||||||
@@ -84,15 +85,14 @@ onMounted(async () => {
|
|||||||
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
||||||
<span class="text-text sm:hidden px-2">Map</span>
|
<span class="text-text sm:hidden px-2">Map</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a
|
<button
|
||||||
title="Bug Report"
|
title="Bug Report"
|
||||||
:href="bugReportUrl"
|
@click="openBugReport"
|
||||||
target="_blank"
|
|
||||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
>
|
>
|
||||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
<span class="text-text sm:hidden px-2">Bug Report</span>
|
<span class="text-text sm:hidden px-2">Bug Report</span>
|
||||||
</a>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Sign Out"
|
title="Sign Out"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
|
|||||||
+7
-1
@@ -10,9 +10,9 @@ import Toast, { POSITION } from 'vue-toastification'
|
|||||||
/* import Vue Lottie */
|
/* import Vue Lottie */
|
||||||
import Vue3Lottie from 'vue3-lottie'
|
import Vue3Lottie from 'vue3-lottie'
|
||||||
import 'vue-toastification/dist/index.css'
|
import 'vue-toastification/dist/index.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import formbricks from './utils/formbricks'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@@ -22,6 +22,12 @@ app.component('font-awesome-icon', FontAwesomeIcon)
|
|||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
if (typeof formbricks !== 'undefined') {
|
||||||
|
formbricks.registerRouteChange()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
position: POSITION.TOP_RIGHT,
|
position: POSITION.TOP_RIGHT,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,15 @@ const useSession = defineStore('session', {
|
|||||||
if (response.tasks) {
|
if (response.tasks) {
|
||||||
this.session.tasks = response.tasks
|
this.session.tasks = response.tasks
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
response.status === 'finished' &&
|
||||||
|
this.session.status !== 'finished'
|
||||||
|
) {
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
this.session.status = response.status
|
this.session.status = response.status
|
||||||
},
|
},
|
||||||
|
finish() {},
|
||||||
reset() {
|
reset() {
|
||||||
socket.reset()
|
socket.reset()
|
||||||
term.reset()
|
term.reset()
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import formbricks from '@formbricks/js/website'
|
||||||
|
import useSession from '../store/session'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
formbricks.init({
|
||||||
|
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
|
||||||
|
apiHost: 'https://app.formbricks.com',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openQuestSurvey() {
|
||||||
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
|
formbricks.track('quest_completed', {
|
||||||
|
hiddenFields: {
|
||||||
|
quest: sessionStore.questId,
|
||||||
|
session: sessionStore.session?._id,
|
||||||
|
username: userStore.username,
|
||||||
|
email: userStore.email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openBugReport() {
|
||||||
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
|
formbricks.track('bug_report', {
|
||||||
|
hiddenFields: {
|
||||||
|
quest: sessionStore.questId,
|
||||||
|
session: sessionStore.session?._id,
|
||||||
|
username: userStore.username,
|
||||||
|
email: userStore.email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formbricks
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import useSession from '../store/session'
|
import useSession from '../store/session'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
||||||
@@ -10,13 +11,22 @@ import VisualizationPart from '../components/game/VisualizationPart.vue'
|
|||||||
import ControlPalette from '../components/game/ControlPalette.vue'
|
import ControlPalette from '../components/game/ControlPalette.vue'
|
||||||
import CompleteModal from '../components/game/CompleteModal.vue'
|
import CompleteModal from '../components/game/CompleteModal.vue'
|
||||||
import StartButton from '../components/game/StartButton.vue'
|
import StartButton from '../components/game/StartButton.vue'
|
||||||
|
import { openQuestSurvey } from '../utils/formbricks'
|
||||||
|
|
||||||
const sessionStore = useSession()
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
const completed = computed(() => {
|
const completed = computed(() => {
|
||||||
return sessionStore.session?.status === 'finished'
|
return sessionStore.session?.status === 'finished'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(completed, (newValue, oldValue) => {
|
||||||
|
if (newValue && !oldValue) {
|
||||||
|
console.log('finish', userStore)
|
||||||
|
openQuestSurvey()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
questId: {
|
questId: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
catelogbg: "url('./catelogbg.svg')",
|
catelogbg: "url('/catelogbg.svg')",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"latest": "^0.2.0",
|
||||||
"typescript-eslint": "^7.4.0"
|
"typescript-eslint": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user