chore: initial commit
This commit is contained in:
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