chore: initial commit

This commit is contained in:
Matthew Raymer
2025-10-15 10:46:50 +00:00
parent 54478b1c97
commit 1e6c4bf7fc
174 changed files with 6867 additions and 21404 deletions

View 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>

View 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;
}

View 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

View 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;
}
}

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>

View File

@@ -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>
Vues
<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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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')

View 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

View 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
}
})

View 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 }
})

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>