chore: initial commit
This commit is contained in:
125
test-apps/daily-notification-test/src/App.vue
Normal file
125
test-apps/daily-notification-test/src/App.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<!--
|
||||
/**
|
||||
* Main App Component - Platform Neutral Vue 3 + vue-facing-decorator
|
||||
*
|
||||
* Cross-platform DailyNotification test app for Android/iOS/Electron
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="app" class="app-container">
|
||||
<!-- Header Navigation -->
|
||||
<AppHeader />
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<AppFooter />
|
||||
|
||||
<!-- Global Loading Overlay -->
|
||||
<LoadingOverlay v-if="isLoading" />
|
||||
|
||||
<!-- Global Error Dialog -->
|
||||
<ErrorDialog
|
||||
v-if="errorMessage"
|
||||
:message="errorMessage"
|
||||
@close="clearError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||
import AppFooter from '@/components/layout/AppFooter.vue'
|
||||
import LoadingOverlay from '@/components/ui/LoadingOverlay.vue'
|
||||
import ErrorDialog from '@/components/ui/ErrorDialog.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const isLoading = computed(() => appStore.isLoading)
|
||||
const errorMessage = computed(() => appStore.errorMessage)
|
||||
|
||||
const initializeApp = async (): Promise<void> => {
|
||||
try {
|
||||
appStore.setLoading(true)
|
||||
|
||||
// Initialize platform detection
|
||||
const platform = Capacitor.getPlatform()
|
||||
const isNative = Capacitor.isNativePlatform()
|
||||
appStore.setPlatform(platform, isNative)
|
||||
|
||||
console.log('🚀 Daily Notification Test App Started')
|
||||
console.log('📱 Platform:', platform)
|
||||
console.log('🔧 Native Platform:', isNative)
|
||||
|
||||
// Check if DailyNotification plugin is available
|
||||
if (isNative && (window as any).DailyNotification) {
|
||||
console.log('✅ DailyNotification plugin available')
|
||||
// Initialize plugin status check
|
||||
await checkPluginStatus()
|
||||
} else if (isNative) {
|
||||
console.warn('⚠️ DailyNotification plugin not available')
|
||||
appStore.setError('DailyNotification plugin not loaded. Please restart the app.')
|
||||
} else {
|
||||
console.log('🌐 Running in web mode - plugin not available')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ App initialization failed:', error)
|
||||
appStore.setError('Failed to initialize app: ' + (error as Error).message)
|
||||
} finally {
|
||||
appStore.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkPluginStatus = async (): Promise<void> => {
|
||||
try {
|
||||
if ((window as any).DailyNotification) {
|
||||
const status = await (window as any).DailyNotification.checkStatus()
|
||||
appStore.setNotificationStatus(status)
|
||||
console.log('📊 Plugin status:', status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Plugin status check failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const clearError = (): void => {
|
||||
appStore.clearError()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeApp()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Platform-specific adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
test-apps/daily-notification-test/src/assets/base.css
Normal file
86
test-apps/daily-notification-test/src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
test-apps/daily-notification-test/src/assets/logo.svg
Normal file
1
test-apps/daily-notification-test/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
35
test-apps/daily-notification-test/src/assets/main.css
Normal file
35
test-apps/daily-notification-test/src/assets/main.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<!--
|
||||
/**
|
||||
* Action Card Component - Platform Neutral Action Card
|
||||
*
|
||||
* Reusable card component for displaying actions
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="action-card" @click="handleClick" :class="{ loading: loading }">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">{{ icon }}</div>
|
||||
<div class="card-text">
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
<div class="card-arrow">
|
||||
<span v-if="loading" class="loading-spinner">⟳</span>
|
||||
<span v-else class="arrow-icon">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class ActionCard extends Vue {
|
||||
@Prop({ required: true })
|
||||
icon!: string
|
||||
|
||||
@Prop({ required: true })
|
||||
title!: string
|
||||
|
||||
@Prop({ required: true })
|
||||
description!: string
|
||||
|
||||
@Prop({ default: false })
|
||||
loading!: boolean
|
||||
|
||||
handleClick(): void {
|
||||
if (!this.loading) {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.action-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.action-card.loading {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
flex-shrink: 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.action-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<!--
|
||||
/**
|
||||
* Status Card Component - Platform Neutral Status Display
|
||||
*
|
||||
* Reusable card component for displaying system status
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="status-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">System Status</h3>
|
||||
<button class="refresh-button" @click="refreshStatus" :disabled="isRefreshing">
|
||||
<span v-if="isRefreshing" class="loading-spinner">⟳</span>
|
||||
<span v-else class="refresh-icon">🔄</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="status-items">
|
||||
<div
|
||||
v-for="item in statusItems"
|
||||
:key="item.label"
|
||||
class="status-item"
|
||||
:class="`status-${item.status}`"
|
||||
>
|
||||
<div class="status-label">{{ item.label }}</div>
|
||||
<div class="status-value">
|
||||
<span class="status-indicator" :class="`indicator-${item.status}`"></span>
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
|
||||
interface StatusItem {
|
||||
label: string
|
||||
value: string
|
||||
status: 'success' | 'error' | 'warning' | 'info'
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class StatusCard extends Vue {
|
||||
@Prop({ required: true })
|
||||
statusItems!: StatusItem[]
|
||||
|
||||
@Prop({ default: false })
|
||||
isRefreshing!: boolean
|
||||
|
||||
refreshStatus(): void {
|
||||
this.$emit('refresh')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.status-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.refresh-button:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.refresh-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-icon, .loading-spinner {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.status-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.status-item.status-success {
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-item.status-error {
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.status-item.status-warning {
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.status-item.status-info {
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.status-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.indicator-success {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.indicator-error {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.indicator-warning {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.indicator-info {
|
||||
background: #2196f3;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.status-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
/**
|
||||
* App Footer Component - Platform Neutral Footer
|
||||
*
|
||||
* Cross-platform footer with version and platform info
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-info">
|
||||
<span class="app-version">v1.0.0</span>
|
||||
<span class="platform-info">{{ platformInfo }}</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link" @click="showAbout">About</a>
|
||||
<a href="#" class="footer-link" @click="showHelp">Help</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
@Component
|
||||
export default class AppFooter extends Vue {
|
||||
private appStore = useAppStore()
|
||||
|
||||
get platformInfo(): string {
|
||||
const platform = this.appStore.platform
|
||||
const isNative = this.appStore.isNative
|
||||
return `${platform.charAt(0).toUpperCase() + platform.slice(1)} ${isNative ? '(Native)' : '(Web)'}`
|
||||
}
|
||||
|
||||
showAbout(): void {
|
||||
// TODO: Show about dialog
|
||||
console.log('About clicked')
|
||||
}
|
||||
|
||||
showHelp(): void {
|
||||
// TODO: Show help dialog
|
||||
console.log('Help clicked')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-footer {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 12px 20px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.platform-info {
|
||||
padding: 2px 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<!--
|
||||
/**
|
||||
* App Header Component - Platform Neutral Navigation
|
||||
*
|
||||
* Cross-platform header with navigation tabs
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="header-content">
|
||||
<!-- App Title -->
|
||||
<div class="app-title">
|
||||
<h1 class="title-text">
|
||||
🔔 Daily Notification Test
|
||||
</h1>
|
||||
<div class="platform-badge" :class="platformClass">
|
||||
{{ platformName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Indicator -->
|
||||
<div class="status-indicator" :class="statusClass">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-text">{{ statusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<nav class="navigation-tabs">
|
||||
<router-link
|
||||
v-for="item in navigationItems"
|
||||
:key="item.name"
|
||||
:to="item.path"
|
||||
class="nav-tab"
|
||||
:class="{ active: $route.name === item.name }"
|
||||
>
|
||||
<span class="nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
interface NavigationItem {
|
||||
name: string
|
||||
path: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class AppHeader extends Vue {
|
||||
private appStore = useAppStore()
|
||||
|
||||
navigationItems: NavigationItem[] = [
|
||||
{ name: 'Home', path: '/', label: 'Home', icon: '🏠' },
|
||||
{ name: 'Schedule', path: '/schedule', label: 'Schedule', icon: '📅' },
|
||||
{ name: 'Notifications', path: '/notifications', label: 'Notifications', icon: '🔔' },
|
||||
{ name: 'Status', path: '/status', label: 'Status', icon: '📊' }
|
||||
]
|
||||
|
||||
get platformName(): string {
|
||||
const platform = this.appStore.platform
|
||||
return platform.charAt(0).toUpperCase() + platform.slice(1)
|
||||
}
|
||||
|
||||
get platformClass(): string {
|
||||
return `platform-${this.appStore.platform}`
|
||||
}
|
||||
|
||||
get statusClass(): string {
|
||||
const status = this.appStore.notificationStatus
|
||||
if (!status) return 'unknown'
|
||||
if (status.canScheduleNow) return 'ready'
|
||||
return 'not-ready'
|
||||
}
|
||||
|
||||
get statusText(): string {
|
||||
const status = this.appStore.notificationStatus
|
||||
if (!status) return 'Unknown'
|
||||
if (status.canScheduleNow) return 'Ready'
|
||||
return 'Not Ready'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-header {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 12px 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.platform-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.platform-android {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.platform-ios {
|
||||
background: rgba(0, 122, 255, 0.2);
|
||||
color: #007aff;
|
||||
border: 1px solid rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.platform-electron {
|
||||
background: rgba(138, 43, 226, 0.2);
|
||||
color: #8a2be2;
|
||||
border: 1px solid rgba(138, 43, 226, 0.3);
|
||||
}
|
||||
|
||||
.platform-web {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-indicator.ready {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator.not-ready {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator.unknown {
|
||||
background: rgba(158, 158, 158, 0.2);
|
||||
color: #9e9e9e;
|
||||
border: 1px solid rgba(158, 158, 158, 0.3);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.navigation-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.2s ease;
|
||||
min-width: 60px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.navigation-tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
min-width: auto;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens - 2 rows */
|
||||
@media (max-width: 480px) {
|
||||
.navigation-tabs {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<!--
|
||||
/**
|
||||
* Error Dialog Component - Platform Neutral Error Display
|
||||
*
|
||||
* Modal dialog for displaying error messages
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="error-dialog-overlay" @click="handleOverlayClick">
|
||||
<div class="error-dialog" @click.stop>
|
||||
<div class="error-header">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h3 class="error-title">Error</h3>
|
||||
</div>
|
||||
|
||||
<div class="error-content">
|
||||
<p class="error-message">{{ message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="error-actions">
|
||||
<button class="error-button secondary" @click="handleClose">
|
||||
Close
|
||||
</button>
|
||||
<button class="error-button primary" @click="handleRetry">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
message: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
retry: []
|
||||
}>()
|
||||
|
||||
const handleClose = (): void => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleRetry = (): void => {
|
||||
emit('retry')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleOverlayClick = (): void => {
|
||||
handleClose()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.error-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.error-button.secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-button.secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.error-button.primary {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-button.primary:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.error-dialog {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
/**
|
||||
* Loading Overlay Component - Platform Neutral Loading
|
||||
*
|
||||
* Full-screen loading overlay with spinner
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Loading Daily Notification Test...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Loading overlay component - no props or logic needed
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 4px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
14
test-apps/daily-notification-test/src/main.ts
Normal file
14
test-apps/daily-notification-test/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
109
test-apps/daily-notification-test/src/router/index.ts
Normal file
109
test-apps/daily-notification-test/src/router/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: HomeView,
|
||||
meta: {
|
||||
title: 'Daily Notification Test',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/schedule',
|
||||
name: 'Schedule',
|
||||
component: () => import('../views/ScheduleView.vue'),
|
||||
meta: {
|
||||
title: 'Schedule Notification',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
name: 'Notifications',
|
||||
component: () => import('../views/NotificationsView.vue'),
|
||||
meta: {
|
||||
title: 'Notification Management',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/status',
|
||||
name: 'Status',
|
||||
component: () => import('../views/StatusView.vue'),
|
||||
meta: {
|
||||
title: 'System Status',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
name: 'History',
|
||||
component: () => import('../views/HistoryView.vue'),
|
||||
meta: {
|
||||
title: 'Notification History',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'Logs',
|
||||
component: () => import('../views/LogsView.vue'),
|
||||
meta: {
|
||||
title: 'System Logs',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('../views/SettingsView.vue'),
|
||||
meta: {
|
||||
title: 'Settings',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
meta: {
|
||||
title: 'About',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFoundView.vue'),
|
||||
meta: {
|
||||
title: 'Page Not Found',
|
||||
requiresAuth: false
|
||||
}
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
// Global navigation guards
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Set page title
|
||||
if (to.meta?.title) {
|
||||
document.title = `${to.meta.title} - Daily Notification Test`
|
||||
}
|
||||
|
||||
// Add loading state
|
||||
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
// Clear any previous errors on successful navigation
|
||||
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
|
||||
})
|
||||
|
||||
export default router
|
||||
109
test-apps/daily-notification-test/src/stores/app.ts
Normal file
109
test-apps/daily-notification-test/src/stores/app.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* App Store - Global Application State
|
||||
*
|
||||
* Pinia store for managing global app state, loading, and errors
|
||||
* Platform-neutral design for Android/iOS/Electron
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface AppState {
|
||||
isLoading: boolean
|
||||
errorMessage: string | null
|
||||
platform: string
|
||||
isNative: boolean
|
||||
notificationStatus: NotificationStatus | null
|
||||
}
|
||||
|
||||
export interface NotificationStatus {
|
||||
canScheduleNow: boolean
|
||||
postNotificationsGranted: boolean
|
||||
channelEnabled: boolean
|
||||
channelImportance: number
|
||||
channelId: string
|
||||
exactAlarmsGranted: boolean
|
||||
exactAlarmsSupported: boolean
|
||||
androidVersion: number
|
||||
nextScheduledAt: number
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
const errorMessage = ref<string | null>(null)
|
||||
const platform = ref('web')
|
||||
const isNative = ref(false)
|
||||
const notificationStatus = ref<NotificationStatus | null>(null)
|
||||
|
||||
// Getters
|
||||
const hasError = computed(() => errorMessage.value !== null)
|
||||
const isNotificationReady = computed(() =>
|
||||
notificationStatus.value?.canScheduleNow ?? false
|
||||
)
|
||||
|
||||
// Actions
|
||||
function setLoading(loading: boolean): void {
|
||||
isLoading.value = loading
|
||||
}
|
||||
|
||||
function setError(message: string): void {
|
||||
errorMessage.value = message
|
||||
console.error('App Error:', message)
|
||||
}
|
||||
|
||||
function clearError(): void {
|
||||
errorMessage.value = null
|
||||
}
|
||||
|
||||
function setPlatform(platformName: string, native: boolean): void {
|
||||
platform.value = platformName
|
||||
isNative.value = native
|
||||
}
|
||||
|
||||
function setNotificationStatus(status: NotificationStatus): void {
|
||||
notificationStatus.value = status
|
||||
}
|
||||
|
||||
function updateNotificationStatus(status: Partial<NotificationStatus>): void {
|
||||
if (notificationStatus.value) {
|
||||
notificationStatus.value = { ...notificationStatus.value, ...status }
|
||||
} else {
|
||||
notificationStatus.value = status as NotificationStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Reset store
|
||||
function $reset(): void {
|
||||
isLoading.value = false
|
||||
errorMessage.value = null
|
||||
platform.value = 'web'
|
||||
isNative.value = false
|
||||
notificationStatus.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
errorMessage,
|
||||
platform,
|
||||
isNative,
|
||||
notificationStatus,
|
||||
|
||||
// Getters
|
||||
hasError,
|
||||
isNotificationReady,
|
||||
|
||||
// Actions
|
||||
setLoading,
|
||||
setError,
|
||||
clearError,
|
||||
setPlatform,
|
||||
setNotificationStatus,
|
||||
updateNotificationStatus,
|
||||
$reset
|
||||
}
|
||||
})
|
||||
12
test-apps/daily-notification-test/src/stores/counter.ts
Normal file
12
test-apps/daily-notification-test/src/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
15
test-apps/daily-notification-test/src/views/AboutView.vue
Normal file
15
test-apps/daily-notification-test/src/views/AboutView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
test-apps/daily-notification-test/src/views/HistoryView.vue
Normal file
54
test-apps/daily-notification-test/src/views/HistoryView.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="history-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">📋 History</h1>
|
||||
<p class="page-subtitle">Notification history and activity</p>
|
||||
</div>
|
||||
<div class="placeholder-content">
|
||||
<p>History view coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class HistoryView extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
332
test-apps/daily-notification-test/src/views/HomeView.vue
Normal file
332
test-apps/daily-notification-test/src/views/HomeView.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<!--
|
||||
/**
|
||||
* Home View - Main Dashboard
|
||||
*
|
||||
* Platform-neutral home view with quick actions
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<!-- Welcome Section -->
|
||||
<div class="welcome-section">
|
||||
<h1 class="welcome-title">
|
||||
🔔 Daily Notification Test
|
||||
</h1>
|
||||
<p class="welcome-subtitle">
|
||||
Vue 3 + vue-facing-decorator + Capacitor
|
||||
</p>
|
||||
<div class="platform-info">
|
||||
<span class="platform-badge" :class="platformClass">
|
||||
{{ platformName }}
|
||||
</span>
|
||||
<span class="status-badge" :class="statusClass">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-actions">
|
||||
<h2 class="section-title">Quick Actions</h2>
|
||||
<div class="action-grid">
|
||||
<ActionCard
|
||||
icon="📅"
|
||||
title="Schedule Notification"
|
||||
description="Schedule a new daily notification"
|
||||
@click="navigateToSchedule"
|
||||
:loading="isScheduling"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="📊"
|
||||
title="Check Status"
|
||||
description="View notification system status"
|
||||
@click="checkSystemStatus"
|
||||
:loading="isCheckingStatus"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="🔔"
|
||||
title="View Notifications"
|
||||
description="Manage scheduled notifications"
|
||||
@click="navigateToNotifications"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="📋"
|
||||
title="View History"
|
||||
description="Check notification history"
|
||||
@click="navigateToHistory"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="📜"
|
||||
title="View Logs"
|
||||
description="Check system logs and debug info"
|
||||
@click="navigateToLogs"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="⚙️"
|
||||
title="Settings"
|
||||
description="Configure app preferences"
|
||||
@click="navigateToSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="system-status">
|
||||
<h2 class="section-title">System Status</h2>
|
||||
<StatusCard :status="systemStatus" @refresh="refreshSystemStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ActionCard from '@/components/cards/ActionCard.vue'
|
||||
import StatusCard from '@/components/cards/StatusCard.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ActionCard,
|
||||
StatusCard
|
||||
}
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
private router = useRouter()
|
||||
private appStore = useAppStore()
|
||||
|
||||
isScheduling = false
|
||||
isCheckingStatus = false
|
||||
|
||||
get platformName(): string {
|
||||
const platform = this.appStore.platform
|
||||
return platform.charAt(0).toUpperCase() + platform.slice(1)
|
||||
}
|
||||
|
||||
get platformClass(): string {
|
||||
return `platform-${this.appStore.platform}`
|
||||
}
|
||||
|
||||
get statusClass(): string {
|
||||
const status = this.appStore.notificationStatus
|
||||
if (!status) return 'unknown'
|
||||
if (status.canScheduleNow) return 'ready'
|
||||
return 'not-ready'
|
||||
}
|
||||
|
||||
get statusText(): string {
|
||||
const status = this.appStore.notificationStatus
|
||||
if (!status) return 'Unknown'
|
||||
if (status.canScheduleNow) return 'Ready'
|
||||
return 'Not Ready'
|
||||
}
|
||||
|
||||
get systemStatus() {
|
||||
const status = this.appStore.notificationStatus
|
||||
if (!status) {
|
||||
return [
|
||||
{ label: 'Platform', value: this.platformName, status: 'info' },
|
||||
{ label: 'Plugin', value: 'Not Available', status: 'error' }
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: 'Platform', value: this.platformName, status: 'success' },
|
||||
{ label: 'Plugin', value: 'Available', status: 'success' },
|
||||
{ label: 'Notifications', value: status.postNotificationsGranted ? 'Granted' : 'Denied', status: status.postNotificationsGranted ? 'success' : 'error' },
|
||||
{ label: 'Exact Alarms', value: status.exactAlarmsGranted ? 'Granted' : 'Denied', status: status.exactAlarmsGranted ? 'success' : 'error' },
|
||||
{ label: 'Channel', value: status.channelEnabled ? 'Enabled' : 'Disabled', status: status.channelEnabled ? 'success' : 'warning' }
|
||||
]
|
||||
}
|
||||
|
||||
navigateToSchedule(): void {
|
||||
this.router.push('/schedule')
|
||||
}
|
||||
|
||||
navigateToNotifications(): void {
|
||||
this.router.push('/notifications')
|
||||
}
|
||||
|
||||
navigateToHistory(): void {
|
||||
this.router.push('/history')
|
||||
}
|
||||
|
||||
navigateToLogs(): void {
|
||||
this.router.push('/logs')
|
||||
}
|
||||
|
||||
navigateToSettings(): void {
|
||||
this.router.push('/settings')
|
||||
}
|
||||
|
||||
async checkSystemStatus(): Promise<void> {
|
||||
this.isCheckingStatus = true
|
||||
try {
|
||||
// Refresh plugin status
|
||||
await this.refreshSystemStatus()
|
||||
} catch (error) {
|
||||
console.error('❌ Status check failed:', error)
|
||||
this.appStore.setError('Failed to check system status: ' + (error as Error).message)
|
||||
} finally {
|
||||
this.isCheckingStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSystemStatus(): Promise<void> {
|
||||
try {
|
||||
if (window.DailyNotification) {
|
||||
const status = await window.DailyNotification.checkStatus()
|
||||
this.appStore.setNotificationStatus(status)
|
||||
console.log('✅ System status refreshed:', status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to refresh system status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 40px 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.platform-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.platform-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.platform-android {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.platform-ios {
|
||||
background: rgba(0, 122, 255, 0.2);
|
||||
color: #007aff;
|
||||
border: 1px solid rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.platform-electron {
|
||||
background: rgba(138, 43, 226, 0.2);
|
||||
color: #8a2be2;
|
||||
border: 1px solid rgba(138, 43, 226, 0.3);
|
||||
}
|
||||
|
||||
.platform-web {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.ready {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.not-ready {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.unknown {
|
||||
background: rgba(158, 158, 158, 0.2);
|
||||
color: #9e9e9e;
|
||||
border: 1px solid rgba(158, 158, 158, 0.3);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.system-status {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.home-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.platform-info {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
475
test-apps/daily-notification-test/src/views/LogsView.vue
Normal file
475
test-apps/daily-notification-test/src/views/LogsView.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<!--
|
||||
/**
|
||||
* Logs View - Platform Neutral Log Display
|
||||
*
|
||||
* View and copy system logs with clipboard functionality
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="logs-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">📋 System Logs</h1>
|
||||
<p class="page-subtitle">View and copy DailyNotification plugin logs</p>
|
||||
</div>
|
||||
|
||||
<div class="logs-controls">
|
||||
<button
|
||||
class="control-button refresh-button"
|
||||
@click="refreshLogs"
|
||||
:disabled="isRefreshing"
|
||||
>
|
||||
<span v-if="isRefreshing">🔄</span>
|
||||
<span v-else>🔄</span>
|
||||
{{ isRefreshing ? 'Refreshing...' : 'Refresh Logs' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="control-button copy-button"
|
||||
@click="copyLogsToClipboard"
|
||||
:disabled="!hasLogs || isCopying"
|
||||
>
|
||||
<span v-if="isCopying">📋</span>
|
||||
<span v-else>📋</span>
|
||||
{{ isCopying ? 'Copying...' : 'Copy to Clipboard' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="control-button clear-button"
|
||||
@click="clearLogs"
|
||||
:disabled="!hasLogs"
|
||||
>
|
||||
🗑️ Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="logs-container">
|
||||
<div v-if="hasLogs" class="logs-content">
|
||||
<div class="logs-header">
|
||||
<span class="logs-count">{{ logs.length }} log entries</span>
|
||||
<span class="last-updated">Last updated: {{ formatTimestamp(lastUpdated) }}</span>
|
||||
</div>
|
||||
<div class="log-entries">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
:class="['log-entry', `log-level-${log.level.toLowerCase()}`]"
|
||||
>
|
||||
<span class="log-timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
||||
<span class="log-level">[{{ log.level }}]</span>
|
||||
<span class="log-tag">{{ log.tag }}:</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-logs">
|
||||
<span class="empty-icon">📜</span>
|
||||
<p class="empty-message">No logs available. Click "Refresh Logs" to fetch them.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="feedbackMessage" :class="['feedback-message', feedbackType]">
|
||||
{{ feedbackMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { format } from 'date-fns'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: number
|
||||
level: string
|
||||
tag: string
|
||||
message: string
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class LogsView extends Vue {
|
||||
private appStore = useAppStore()
|
||||
|
||||
logs: LogEntry[] = []
|
||||
isRefreshing = false
|
||||
isCopying = false
|
||||
lastUpdated: number = Date.now()
|
||||
feedbackMessage: string | null = null
|
||||
feedbackType: 'success' | 'error' = 'success'
|
||||
|
||||
get hasLogs(): boolean {
|
||||
return this.logs.length > 0
|
||||
}
|
||||
|
||||
mounted(): void {
|
||||
this.refreshLogs()
|
||||
}
|
||||
|
||||
async refreshLogs(): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
this.appStore.setError('Log fetching is only available on native platforms.')
|
||||
return
|
||||
}
|
||||
|
||||
this.isRefreshing = true
|
||||
this.clearFeedback()
|
||||
|
||||
try {
|
||||
// For now, create mock logs - replace with actual plugin call
|
||||
// Example: const rawLogs = await DailyNotificationPlugin.getLogs();
|
||||
const mockLogs: LogEntry[] = [
|
||||
{ timestamp: Date.now() - 5000, level: 'DEBUG', tag: 'DailyNotificationPlugin', message: 'Plugin initialized' },
|
||||
{ timestamp: Date.now() - 4000, level: 'INFO', tag: 'DailyNotificationScheduler', message: 'Notification scheduled for 09:00' },
|
||||
{ timestamp: Date.now() - 3000, level: 'WARN', tag: 'DailyNotificationWorker', message: 'JIT freshness check skipped: content too fresh' },
|
||||
{ timestamp: Date.now() - 2000, level: 'ERROR', tag: 'DailyNotificationStorage', message: 'Failed to save notification: disk full' },
|
||||
{ timestamp: Date.now() - 1000, level: 'INFO', tag: 'DailyNotificationReceiver', message: 'Received NOTIFICATION intent' }
|
||||
]
|
||||
|
||||
this.logs = mockLogs.sort((a, b) => a.timestamp - b.timestamp)
|
||||
this.lastUpdated = Date.now()
|
||||
this.showFeedback(`✅ Fetched ${this.logs.length} log entries.`, 'success')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to refresh logs:', error)
|
||||
this.appStore.setError('Failed to fetch logs: ' + (error as Error).message)
|
||||
this.showFeedback('❌ Failed to fetch logs.', 'error')
|
||||
} finally {
|
||||
this.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
async copyLogsToClipboard(): Promise<void> {
|
||||
if (!this.hasLogs) {
|
||||
this.showFeedback('No logs to copy.', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
this.isCopying = true
|
||||
this.clearFeedback()
|
||||
const logsText = this.formatLogsForCopy()
|
||||
|
||||
try {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
// Use Capacitor Clipboard plugin if available
|
||||
// Assuming Clipboard plugin is installed and configured
|
||||
await window.Capacitor.Plugins.Clipboard.write({
|
||||
string: logsText
|
||||
})
|
||||
} else {
|
||||
// Web clipboard API
|
||||
await navigator.clipboard.writeText(logsText)
|
||||
}
|
||||
|
||||
this.showFeedback(`✅ Copied ${this.logs.length} log entries to clipboard!`, 'success')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to copy logs:', error)
|
||||
this.appStore.setError('Failed to copy logs: ' + (error as Error).message)
|
||||
this.showFeedback('❌ Failed to copy logs.', 'error')
|
||||
} finally {
|
||||
this.isCopying = false
|
||||
}
|
||||
}
|
||||
|
||||
clearLogs(): void {
|
||||
this.logs = []
|
||||
this.clearFeedback()
|
||||
this.showFeedback('🗑️ Logs cleared.', 'success')
|
||||
}
|
||||
|
||||
formatTimestamp(timestamp: number): string {
|
||||
return format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')
|
||||
}
|
||||
|
||||
formatLogsForCopy(): string {
|
||||
let formattedText = `DailyNotification Plugin Logs\n`
|
||||
formattedText += `Generated: ${format(new Date(), 'MM/dd/yyyy, h:mm:ss a')}\n`
|
||||
formattedText += `Total Entries: ${this.logs.length}\n\n`
|
||||
|
||||
this.logs.forEach(log => {
|
||||
formattedText += `${this.formatTimestamp(log.timestamp)} ${log.level}/${log.tag}: ${log.message}\n`
|
||||
})
|
||||
|
||||
return formattedText
|
||||
}
|
||||
|
||||
showFeedback(message: string, type: 'success' | 'error'): void {
|
||||
this.feedbackMessage = message
|
||||
this.feedbackType = type
|
||||
setTimeout(() => {
|
||||
this.feedbackMessage = null
|
||||
}, 3000) // Hide after 3 seconds
|
||||
}
|
||||
|
||||
clearFeedback(): void {
|
||||
this.feedbackMessage = null
|
||||
this.feedbackType = 'success'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.refresh-button:hover:not(:disabled) {
|
||||
background: #1976d2;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.copy-button:hover:not(:disabled) {
|
||||
background: #f57c00;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.clear-button:hover:not(:disabled) {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.control-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logs-count {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.log-entries {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-level-debug {
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
border-left: 3px solid #2196f3;
|
||||
}
|
||||
|
||||
.log-level-info {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
.log-level-warn {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.log-level-error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-left: 3px solid #f44336;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.log-level-debug .log-level {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.log-level-info .log-level {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.log-level-warn .log-level {
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.log-level-error .log-level {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.log-tag {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.no-logs {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.feedback-message {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.feedback-message.success {
|
||||
background: rgba(76, 175, 80, 0.9);
|
||||
color: white;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.feedback-message.error {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
color: white;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.logs-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
test-apps/daily-notification-test/src/views/NotFoundView.vue
Normal file
70
test-apps/daily-notification-test/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="not-found-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">404</h1>
|
||||
<p class="page-subtitle">Page not found</p>
|
||||
</div>
|
||||
<div class="placeholder-content">
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
<router-link to="/" class="home-link">← Back to Home</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class NotFoundView extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-found-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.home-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.home-link:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="notifications-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">🔔 Notifications</h1>
|
||||
<p class="page-subtitle">Manage scheduled notifications</p>
|
||||
</div>
|
||||
<div class="placeholder-content">
|
||||
<p>Notifications management coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class NotificationsView extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notifications-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
180
test-apps/daily-notification-test/src/views/ScheduleView.vue
Normal file
180
test-apps/daily-notification-test/src/views/ScheduleView.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<!--
|
||||
/**
|
||||
* Schedule View - Platform Neutral Scheduling Interface
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="schedule-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">📅 Schedule Notification</h1>
|
||||
<p class="page-subtitle">Schedule a new daily notification</p>
|
||||
</div>
|
||||
|
||||
<div class="schedule-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notification Time</label>
|
||||
<input type="time" class="form-input" v-model="scheduleTime" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-input" v-model="notificationTitle" placeholder="Daily Update" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Message</label>
|
||||
<textarea class="form-textarea" v-model="notificationMessage" placeholder="Your daily notification message"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="action-button primary" @click="scheduleNotification" :disabled="isScheduling">
|
||||
{{ isScheduling ? 'Scheduling...' : 'Schedule Notification' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class ScheduleView extends Vue {
|
||||
scheduleTime = '09:00'
|
||||
notificationTitle = 'Daily Update'
|
||||
notificationMessage = 'Your daily notification is ready!'
|
||||
isScheduling = false
|
||||
|
||||
async scheduleNotification(): Promise<void> {
|
||||
this.isScheduling = true
|
||||
try {
|
||||
// TODO: Implement actual scheduling
|
||||
console.log('Scheduling notification:', {
|
||||
time: this.scheduleTime,
|
||||
title: this.notificationTitle,
|
||||
message: this.notificationMessage
|
||||
})
|
||||
|
||||
// Mock success
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
console.log('✅ Notification scheduled successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to schedule notification:', error)
|
||||
} finally {
|
||||
this.isScheduling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schedule-view {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.schedule-form {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.primary:hover:not(:disabled) {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.schedule-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.schedule-form {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
test-apps/daily-notification-test/src/views/SettingsView.vue
Normal file
54
test-apps/daily-notification-test/src/views/SettingsView.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="settings-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">⚙️ Settings</h1>
|
||||
<p class="page-subtitle">Configure app preferences</p>
|
||||
</div>
|
||||
<div class="placeholder-content">
|
||||
<p>Settings view coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class SettingsView extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
54
test-apps/daily-notification-test/src/views/StatusView.vue
Normal file
54
test-apps/daily-notification-test/src/views/StatusView.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="status-view">
|
||||
<div class="view-header">
|
||||
<h1 class="page-title">📊 Status</h1>
|
||||
<p class="page-subtitle">System status and diagnostics</p>
|
||||
</div>
|
||||
<div class="placeholder-content">
|
||||
<p>Status view coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class StatusView extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.status-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user