From f56477cf3423b0ab29749e4288bb499fde041853 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 16 Sep 2025 17:10:37 -0400 Subject: [PATCH 01/27] WIP: Start working on rating component. --- src/components/UserExperience.vue | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/components/UserExperience.vue diff --git a/src/components/UserExperience.vue b/src/components/UserExperience.vue new file mode 100644 index 0000000..1109e35 --- /dev/null +++ b/src/components/UserExperience.vue @@ -0,0 +1,75 @@ + + + + + From 3226434468d867004930ad1ff5964c9a83613f21 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 19 Sep 2025 10:46:57 -0400 Subject: [PATCH 02/27] Use textarea in rating component and export from package. Add notifications to Storybook app. --- .storybook/preview.ts | 2 + src/components/UserExperience.vue | 135 ++++++++++++++++++++++++------ src/index.ts | 2 + src/types.ts | 7 ++ src/utils.ts | 36 ++++++++ 5 files changed, 158 insertions(+), 24 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 489d44b..88ccaa0 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -4,6 +4,7 @@ import { wwtPinia } from "@wwtelescope/engine-pinia"; import { createVuetify } from 'vuetify'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; +import Notifications from "@kyvg/vue3-notification"; const vuetify = createVuetify({ components, @@ -13,6 +14,7 @@ const vuetify = createVuetify({ setup((app) => { app.use(wwtPinia); app.use(vuetify); + app.use(Notifications); }); const preview: Preview = { diff --git a/src/components/UserExperience.vue b/src/components/UserExperience.vue index 1109e35..16bf274 100644 --- a/src/components/UserExperience.vue +++ b/src/components/UserExperience.vue @@ -1,26 +1,51 @@ diff --git a/src/index.ts b/src/index.ts index 5917d6e..fb41d83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import PlaybackControl from "./components/PlaybackControl.vue"; import ShareButton from "./components/ShareButton.vue"; import SpeedControl from "./components/SpeedControl.vue"; import TapToInput from "./components/TapToInput.vue"; +import UserExperience from "./components/UserExperience.vue"; import WwtHud from "./components/WwtHud.vue"; export { @@ -48,6 +49,7 @@ export { ShareButton, SpeedControl, TapToInput, + UserExperience, WwtHud, }; diff --git a/src/types.ts b/src/types.ts index cac41de..0037bd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -394,3 +394,10 @@ export interface ShareButtonProps { /** The ARIA label for the button */ ariaLabel?: string; } + +export interface UserExperienceProps { + apiKey: string; + baseColor?: string; + ratingColors: string[]; + story: string; +} diff --git a/src/utils.ts b/src/utils.ts index d1dea0e..9dd82e6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -56,3 +56,39 @@ export function filterInPlace(array: T[], condition: (t: T) => boolean) { array.length = j; } + +export type UserExperienceRating = "very_bad" | "poor" | "medium" | "good" | "excellent"; +export const DEFAULT_RATING_COLORS = ["red", "orange", "yellow", "lightgreen", "green"]; + +export interface UserExperienceSubmissionInfo { + // eslint-disable-next-line @typescript-eslint/naming-convention + story_name: string; + uuid: string; + comments?: string; + rating?: UserExperienceRating; +} + +export async function submitUserExperienceRating(info: UserExperienceSubmissionInfo, apiKey: string): Promise { + const body: UserExperienceSubmissionInfo = { + // eslint-disable-next-line @typescript-eslint/naming-convention + story_name: info.story_name, + uuid: info.uuid, + }; + if (info.comments) { + body.comments = info.comments; + } + if (info.rating) { + body.rating == info.rating; + } + return fetch(`${API_BASE_URL}/stories/user-experience`, { + method: "PUT", + body: JSON.stringify(body), + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { "Authorization": apiKey }, + }) + .then(response => response.status === 200) + .catch(error => { + console.error(error); + return false; + }); +} From 665bd75c219ebc597b8d18807e9eeff11ed09822 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 19 Sep 2025 12:08:44 -0400 Subject: [PATCH 03/27] Add storybook story. --- src/stories/UserExperience.stories.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/stories/UserExperience.stories.ts diff --git a/src/stories/UserExperience.stories.ts b/src/stories/UserExperience.stories.ts new file mode 100644 index 0000000..cd31665 --- /dev/null +++ b/src/stories/UserExperience.stories.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { Meta, StoryObj } from "@storybook/vue3"; +import { UserExperienceProps } from "../types"; +import { UserExperience } from ".."; + +const meta: Meta = { + component: UserExperience, + tags: ["autodocs"], + title: "Vue Toolkit/Components/User Experience", +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: (args: UserExperienceProps) => ({ + components: { UserExperience }, + template: ``, + setup() { + return { args }; + } + }), + args: { + baseColor: "black", + } +}; From 15b9753ac36e1c3f339425d1aa5ef9c9dd101637 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sat, 20 Sep 2025 00:27:57 -0400 Subject: [PATCH 04/27] More work on updating component and story. --- src/stories/UserExperience.stories.ts | 45 +++++++++++++++++++++++---- src/types.ts | 2 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/stories/UserExperience.stories.ts b/src/stories/UserExperience.stories.ts index cd31665..4ff62b5 100644 --- a/src/stories/UserExperience.stories.ts +++ b/src/stories/UserExperience.stories.ts @@ -13,15 +13,48 @@ const meta: Meta = { export default meta; type Story = StoryObj; +import { notify } from "@kyvg/vue3-notification"; + export const Primary: Story = { - render: (args: UserExperienceProps) => ({ - components: { UserExperience }, - template: ``, - setup() { - return { args }; + render: (args: UserExperienceProps) => { + + function submitHandler(response: boolean) { + console.log(`Handler: ${response}`); + const type = response ? "success" : "error"; + const text = response ? + "Your feedback was submitted successfully!" : + "There was an issue submitting your feedback"; + console.log(notify); + notify({ + group: "rating-submission", + type, + text, + duration: 4500, + }); } - }), + + return { + components: { UserExperience }, + template: ` +
+ + +
+ `, + setup() { + return { + args, + submitHandler, + }; + }, + }; + }, args: { baseColor: "black", + apiKey: process.env.VUE_APP_CDS_API_KEY, + story: "storybook", } }; diff --git a/src/types.ts b/src/types.ts index 0037bd7..33ab846 100644 --- a/src/types.ts +++ b/src/types.ts @@ -398,6 +398,6 @@ export interface ShareButtonProps { export interface UserExperienceProps { apiKey: string; baseColor?: string; - ratingColors: string[]; + ratingColors?: string[]; story: string; } From 4bad294f05aea75101eba7e330e21bf9898b5a5d Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 24 Sep 2025 18:17:30 -0400 Subject: [PATCH 05/27] WIP: Add 'attention hook' component. --- src/components/AttentionHook.vue | 82 ++++++++++++++++++++++++++++ src/index.ts | 2 + src/stories/AttentionHook.stories.ts | 33 +++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/components/AttentionHook.vue create mode 100644 src/stories/AttentionHook.stories.ts diff --git a/src/components/AttentionHook.vue b/src/components/AttentionHook.vue new file mode 100644 index 0000000..36793de --- /dev/null +++ b/src/components/AttentionHook.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/index.ts b/src/index.ts index fb41d83..0f6fdc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { usePlaybackControl } from "./composables/playbackControl"; import { useWindowShape, WindowShape } from "./composables/windowShape"; import { useWWTKeyboardControls } from "./composables/wwtKeyboard"; +import AttentionHook from "./components/AttentionHook.vue"; import CreditLogos from "./components/CreditLogos.vue"; import DateTimePicker from "./components/DateTimePicker.vue"; import FundingAcknowledgement from "./components/FundingAcknowledgement.vue"; @@ -37,6 +38,7 @@ export { useWindowShape, useWWTKeyboardControls, + AttentionHook, CreditLogos, DateTimePicker, FundingAcknowledgement, diff --git a/src/stories/AttentionHook.stories.ts b/src/stories/AttentionHook.stories.ts new file mode 100644 index 0000000..b089c46 --- /dev/null +++ b/src/stories/AttentionHook.stories.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { Meta, StoryObj } from "@storybook/vue3"; +import { AttentionHook } from ".."; + +const meta: Meta = { + component: AttentionHook, + tags: ["autodocs"], + title: "Vue Toolkit/Components/Attention Hook", +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: (args: any) => { + + return { + components: { AttentionHook }, + template: ` +
+ + +
+ `, + setup() { + return { args }; + } + }; + }, + args: { + } +}; From 50a32ca8614ea89e71fdd77bb371b085f6e64ac7 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 25 Sep 2025 01:06:15 -0400 Subject: [PATCH 06/27] Work out details of attention hook animation. --- src/components/AttentionHook.vue | 54 ++++++++++++++++++++-------- src/stories/AttentionHook.stories.ts | 5 +++ src/utils.ts | 23 ++++++++++++ 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/components/AttentionHook.vue b/src/components/AttentionHook.vue index 36793de..15fc07a 100644 --- a/src/components/AttentionHook.vue +++ b/src/components/AttentionHook.vue @@ -2,7 +2,7 @@
diff --git a/src/stories/AttentionHook.stories.ts b/src/stories/AttentionHook.stories.ts index 701a097..398ea5e 100644 --- a/src/stories/AttentionHook.stories.ts +++ b/src/stories/AttentionHook.stories.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Meta, StoryObj } from "@storybook/vue3"; -import { AttentionHook, UserExperience } from ".."; +import { AttentionHook, submitUserExperienceRating, UserExperience, UserExperienceSubmissionInfo, API_BASE_URL } from ".."; import { ref } from "vue"; +import { notify } from "@kyvg/vue3-notification"; const meta: Meta = { component: AttentionHook, @@ -14,6 +15,7 @@ export default meta; type Story = StoryObj; export const Primary: Story = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any render: (args: any) => { return { @@ -38,13 +40,31 @@ export const Primary: Story = { } }; -console.log(process.env.VUE_APP_CDS_API_KEY); +function submitter(info: UserExperienceSubmissionInfo, apiKey: string): Promise { + return submitUserExperienceRating(info, apiKey, `${API_BASE_URL}/storybook/user-experience`); +} + +const showHook = ref(true); +const showExperience = ref(false); +function submitResponseHandler(response: Response | null) { + const success = response?.status === 200; + const type = success ? "success" : "error"; + const text = success ? + "Your feedback was submitted successfully!" : + "There was an issue submitting your feedback"; + notify({ + group: "rating-submission", + type, + text, + duration: 4500, + closeOnClick: false, + }); + showExperience.value = false; +} export const WithUserExperience: Story = { render: (args: any) => { - const showHook = ref(true); - const showExperience = ref(false); return { components: { AttentionHook, UserExperience }, template: ` @@ -53,18 +73,19 @@ export const WithUserExperience: Story = { v-bind="args" v-if="showHook" @open="() => { - console.log('open'); showHook = false; showExperience = true; }" > +
`, setup() { @@ -72,6 +93,7 @@ export const WithUserExperience: Story = { args, showHook, showExperience, + submitResponseHandler, }; } }; @@ -89,5 +111,6 @@ export const WithUserExperience: Story = { // We don't need these responses, so just use the same UUID for everyone uuid: "42274bf4-4228-4cb0-951b-18cbce176189", + submitter, } }; diff --git a/src/types.ts b/src/types.ts index 1b78823..ae041ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { CircleMarkerOptions, TileLayerOptions } from "leaflet"; import { engineStore } from "@wwtelescope/engine-pinia"; import { MapBoxFeatureCollection } from "./mapbox"; +import { UserExperienceSubmissionInfo } from "./utils"; /** The type of the WWT engine Pinia store */ export type WWTEngineStore = ReturnType; @@ -397,8 +398,10 @@ export interface ShareButtonProps { export interface UserExperienceProps { apiKey: string; + uuid: string; baseColor?: string; ratingColors?: string[]; story: string; commentPlaceholder?: string; + submitter: (info: UserExperienceSubmissionInfo, apiKey: string) => Promise; } diff --git a/src/utils.ts b/src/utils.ts index 22eca04..38b3e91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,8 +4,7 @@ export const D2R = Math.PI / 180; export const R2D = 180 / Math.PI; /** The base URL for the CosmicDS API server */ -// export const API_BASE_URL = "https://api.cosmicds.cfa.harvard.edu"; -export const API_BASE_URL = "http://localhost:8081"; +export const API_BASE_URL = "https://api.cosmicds.cfa.harvard.edu"; /** * Determine whether the user's device supports touch events. @@ -69,7 +68,11 @@ export interface UserExperienceSubmissionInfo { rating?: UserExperienceRating; } -export async function submitUserExperienceRating(info: UserExperienceSubmissionInfo, apiKey: string): Promise { +export async function submitUserExperienceRating( + info: UserExperienceSubmissionInfo, + apiKey: string, + url?: string +): Promise { const body: UserExperienceSubmissionInfo = { // eslint-disable-next-line @typescript-eslint/naming-convention story_name: info.story_name, @@ -81,7 +84,8 @@ export async function submitUserExperienceRating(info: UserExperienceSubmissionI if (info.rating) { body.rating == info.rating; } - return fetch(`${API_BASE_URL}/stories/user-experience`, { + const destination = url ?? `${API_BASE_URL}/stories/user-experience`; + return fetch(destination, { method: "PUT", body: JSON.stringify(body), headers: { @@ -91,10 +95,9 @@ export async function submitUserExperienceRating(info: UserExperienceSubmissionI "Content-Type": "application/json", }, }) - .then(response => response.status === 200) .catch(error => { console.error(error); - return false; + return null; }); } From 887b7c918f97d8ebc73a29c8dc1ef89346765add Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 25 Sep 2025 16:58:57 -0400 Subject: [PATCH 10/27] Allow specifying bounce count. --- src/components/AttentionHook.vue | 3 +++ src/stories/AttentionHook.stories.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/components/AttentionHook.vue b/src/components/AttentionHook.vue index 922372c..dab21ec 100644 --- a/src/components/AttentionHook.vue +++ b/src/components/AttentionHook.vue @@ -30,6 +30,7 @@ interface Props { bounceAmount?: string; bounceDuration?: number; betweenBouncesDuration?: number; + bounceCount?: number; popupTime?: number; } @@ -38,6 +39,7 @@ const props = withDefaults(defineProps(), { bounceAmount: "10%", bounceDuration: 500, betweenBouncesDuration: 1000, + bounceCount: Infinity, popupTime: 500, }); @@ -87,6 +89,7 @@ const emit = defineEmits<{ align-items: center; justify-content: center; border-radius: 10px; + overscroll-behavior-y: contain; &:hover { cursor: pointer; diff --git a/src/stories/AttentionHook.stories.ts b/src/stories/AttentionHook.stories.ts index 398ea5e..ef35527 100644 --- a/src/stories/AttentionHook.stories.ts +++ b/src/stories/AttentionHook.stories.ts @@ -63,6 +63,7 @@ function submitResponseHandler(response: Response | null) { } export const WithUserExperience: Story = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any render: (args: any) => { return { @@ -103,6 +104,7 @@ export const WithUserExperience: Story = { bounceAmount: "10%", bounceDuration: 500, betweenBouncesDuration: 1000, + bounceCount: 3, popupTime: 500, baseColor: "black", From 60190a03178752878f9f11803a57b974266ac69f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 25 Sep 2025 16:59:36 -0400 Subject: [PATCH 11/27] Bounce up rather than down. --- src/utils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 38b3e91..22d0757 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -105,6 +105,7 @@ export interface BounceAnimationProperties { bounceAmount: string; bounceDuration: number; betweenBouncesDuration: number; + bounceCount: number; } export function createBounceKeyframes(props: BounceAnimationProperties): Keyframe[] { @@ -112,7 +113,7 @@ export function createBounceKeyframes(props: BounceAnimationProperties): Keyfram const bounceFraction = props.bounceDuration / totalDuration; return [ { transform: "translateY(0)", offset: 0 }, - { transform: `translateY(${props.bounceAmount})`, offset: 0.5 * bounceFraction }, + { transform: `translateY(-${props.bounceAmount})`, offset: 0.5 * bounceFraction }, { transform: "translateY(0)", offset: bounceFraction }, ]; } @@ -120,6 +121,10 @@ export function createBounceKeyframes(props: BounceAnimationProperties): Keyfram export function createBounceAnimation(element: HTMLElement, props: BounceAnimationProperties): Animation { const keyframes = createBounceKeyframes(props); const totalDuration = props.bounceDuration + props.betweenBouncesDuration; - const animation = element.animate(keyframes, { duration: totalDuration, iterations: Infinity }); + console.log(props); + const animation = element.animate(keyframes, { + duration: totalDuration, + iterations: props.bounceCount + }); return animation; } From f0a98c7b1327a1f2d4a495bf2cf0c811b8203544 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 25 Sep 2025 17:07:34 -0400 Subject: [PATCH 12/27] Add type annotations to fix build. --- src/components/UserExperience.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/UserExperience.vue b/src/components/UserExperience.vue index c99a943..25fbcd8 100644 --- a/src/components/UserExperience.vue +++ b/src/components/UserExperience.vue @@ -9,7 +9,7 @@ -