Skip to main content

Mobile App Guide

This guide covers how to use Shyfts on mobile devices, including touch interactions, responsive features, and mobile-optimised workflows.


Overview

Progressive Web App

Shyfts is a Progressive Web App (PWA) that works on any mobile device:

FeatureDescription
No App StoreAccess via web browser
Home ScreenAdd to home screen for app-like experience
Offline SupportView cached data offline
Push NotificationsReceive alerts (when enabled)
Responsive DesignOptimised for all screen sizes

Supported Devices

PlatformBrowserVersion
iOSSafari14+
iOSChromeLatest
AndroidChrome80+
AndroidSamsung InternetLatest
AndroidFirefoxLatest

Getting Started

Accessing Shyfts Mobile

  1. Open your mobile browser
  2. Navigate to your Shyfts URL
  3. Login with your credentials
  4. You're ready to use Shyfts

Add to Home Screen

iPhone/iPad (Safari):

  1. Open Shyfts in Safari
  2. Tap the Share button (square with arrow)
  3. Scroll down and tap Add to Home Screen
  4. Name it "Shyfts" and tap Add

Android (Chrome):

  1. Open Shyfts in Chrome
  2. Tap the Menu (three dots)
  3. Tap Add to Home Screen
  4. Tap Add

Home Screen Icon

Once added, Shyfts appears as an app icon:

📱 Your Home Screen
├── 🩺 Shyfts (full-screen experience)
├── 📱 Other Apps
└── ...

Mobile Navigation

Bottom Navigation Bar

On mobile, the main navigation appears at the bottom:

IconSectionDescription
🏠DashboardMain overview
📅ScheduleYour shifts
Time ClockClock in/out
📋LeaveLeave requests
👤ProfileYour settings
// Mobile bottom navigation
const MobileNavigation = () => (
<nav className="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur-md border-t border-white/10 safe-area-bottom">
<div className="flex justify-around py-2">
<NavItem href="/staff" icon={Home} label="Home" />
<NavItem href="/staff/schedule" icon={Calendar} label="Schedule" />
<NavItem href="/staff/time-clock" icon={Clock} label="Clock" />
<NavItem href="/staff/leave" icon={FileText} label="Leave" />
<NavItem href="/staff/profile" icon={User} label="Profile" />
</div>
</nav>
)

// Navigation item with touch target
const NavItem = ({ href, icon: Icon, label }) => (
<Link
href={href}
className="flex flex-col items-center justify-center min-w-[64px] min-h-[44px] p-2"
>
<Icon className="w-6 h-6" />
<span className="text-xs mt-1">{label}</span>
</Link>
)

Safe Area Handling

// Handle iOS safe areas (notch, home indicator)
const safeAreaStyles = `
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);
}

.safe-area-top {
padding-top: env(safe-area-inset-top, 0);
}
`

Dashboard

Mobile Dashboard Layout

// Mobile dashboard layout
const MobileDashboard = () => (
<div className="min-h-screen pb-20">
{/* Header */}
<header className="sticky top-0 bg-background/95 backdrop-blur-md z-10 p-4 safe-area-top">
<h1 className="text-xl font-semibold">Dashboard</h1>
<p className="text-sm text-secondary-text">
{formatDate(new Date(), 'dddd, D MMMM')}
</p>
</header>

{/* Quick Actions */}
<div className="px-4 py-3">
<QuickActions />
</div>

{/* Today's Shift */}
<section className="px-4 py-3">
<h2 className="text-lg font-medium mb-3">Today</h2>
<TodayShiftCard />
</section>

{/* Upcoming Shifts */}
<section className="px-4 py-3">
<h2 className="text-lg font-medium mb-3">This Week</h2>
<UpcomingShiftsList />
</section>

{/* Notifications */}
<section className="px-4 py-3">
<h2 className="text-lg font-medium mb-3">Notifications</h2>
<NotificationsList />
</section>
</div>
)

Quick Actions

// Mobile quick action buttons
const QuickActions = () => (
<div className="grid grid-cols-2 gap-3">
<ActionButton
icon={Clock}
label="Clock In"
onClick={handleClockIn}
variant="primary"
/>
<ActionButton
icon={Calendar}
label="Request Leave"
onClick={() => navigate('/staff/leave/request')}
variant="secondary"
/>
</div>
)

// Action button with 44px+ touch target
const ActionButton = ({ icon: Icon, label, onClick, variant }) => (
<button
onClick={onClick}
className={`
flex items-center justify-center gap-2 p-4 rounded-xl
min-h-[48px] active:scale-95 transition-transform
${variant === 'primary'
? 'bg-coral-gradient text-white'
: 'bg-white/10 text-white'}
`}
>
<Icon className="w-5 h-5" />
<span className="font-medium">{label}</span>
</button>
)

Schedule View

Mobile Calendar

// Mobile schedule calendar
const MobileSchedule = () => {
const [view, setView] = useState<'day' | 'week'>('day')
const [date, setDate] = useState(new Date())

return (
<div className="min-h-screen pb-20">
{/* Header with date picker */}
<header className="sticky top-0 bg-background/95 backdrop-blur-md z-10 p-4">
<div className="flex items-center justify-between">
<button
onClick={() => navigateDate(-1)}
className="p-3 min-w-[44px] min-h-[44px]"
>
<ChevronLeft className="w-6 h-6" />
</button>

<div className="text-center">
<div className="text-lg font-semibold">
{formatDate(date, view === 'day' ? 'ddd, D MMM' : 'Week of D MMM')}
</div>
</div>

<button
onClick={() => navigateDate(1)}
className="p-3 min-w-[44px] min-h-[44px]"
>
<ChevronRight className="w-6 h-6" />
</button>
</div>

{/* View toggle */}
<div className="flex gap-2 mt-3">
<ViewToggle
value={view}
onChange={setView}
options={['day', 'week']}
/>
</div>
</header>

{/* Shift list */}
<div className="p-4 space-y-3">
{shifts.map(shift => (
<MobileShiftCard key={shift.id} shift={shift} />
))}
</div>
</div>
)
}

Mobile Shift Card

// Mobile-optimised shift card
const MobileShiftCard = ({ shift }) => (
<div
className="card-glass p-4 active:scale-[0.98] transition-transform"
onClick={() => viewShiftDetails(shift.id)}
>
<div className="flex justify-between items-start">
<div>
<div className="font-semibold">
{formatTime(shift.start_time)} - {formatTime(shift.end_time)}
</div>
<div className="text-sm text-secondary-text">
{shift.duration}
</div>
</div>
<ShiftStatusBadge status={shift.status} size="sm" />
</div>

{shift.room && (
<div className="mt-3 flex items-center gap-2 text-sm">
<MapPin className="w-4 h-4 text-secondary-text" />
<span>{shift.room.name}</span>
</div>
)}

{shift.notes && (
<div className="mt-2 text-sm text-secondary-text line-clamp-2">
{shift.notes}
</div>
)}
</div>
)

Swipe Gestures

// Swipe navigation for calendar
const useSwipeNavigation = (onSwipeLeft: () => void, onSwipeRight: () => void) => {
const [touchStart, setTouchStart] = useState<number | null>(null)
const [touchEnd, setTouchEnd] = useState<number | null>(null)

const minSwipeDistance = 50

const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null)
setTouchStart(e.targetTouches[0].clientX)
}

const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX)
}

const onTouchEnd = () => {
if (!touchStart || !touchEnd) return

const distance = touchStart - touchEnd
const isLeftSwipe = distance > minSwipeDistance
const isRightSwipe = distance < -minSwipeDistance

if (isLeftSwipe) onSwipeLeft()
if (isRightSwipe) onSwipeRight()
}

return { onTouchStart, onTouchMove, onTouchEnd }
}

Time Clock

Mobile Clock In/Out

// Mobile time clock screen
const MobileTimeClock = () => {
const { currentStatus, clockIn, clockOut, startBreak, endBreak } = useTimeClock()

return (
<div className="min-h-screen pb-20 flex flex-col">
{/* Header */}
<header className="p-4 text-center">
<h1 className="text-xl font-semibold">Time Clock</h1>
<p className="text-secondary-text">
{formatDate(new Date(), 'dddd, D MMMM YYYY')}
</p>
</header>

{/* Current Time Display */}
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl font-bold tabular-nums">
{formatTime(new Date(), 'HH:mm')}
</div>
<div className="text-xl text-secondary-text mt-2">
{formatTime(new Date(), 'ss')}
</div>
</div>
</div>

{/* Status */}
<div className="px-4 py-6 text-center">
<ClockStatusBadge status={currentStatus} />
{currentStatus === 'clocked_in' && (
<div className="mt-2 text-secondary-text">
Working for {getElapsedTime()}
</div>
)}
</div>

{/* Action Buttons */}
<div className="p-4 space-y-3">
{currentStatus === 'not_clocked_in' && (
<ClockButton
label="Clock In"
icon={LogIn}
onClick={clockIn}
variant="primary"
/>
)}

{currentStatus === 'clocked_in' && (
<>
<ClockButton
label="Start Break"
icon={Coffee}
onClick={startBreak}
variant="secondary"
/>
<ClockButton
label="Clock Out"
icon={LogOut}
onClick={clockOut}
variant="danger"
/>
</>
)}

{currentStatus === 'on_break' && (
<ClockButton
label="End Break"
icon={PlayCircle}
onClick={endBreak}
variant="primary"
/>
)}
</div>
</div>
)
}

Large Touch Buttons

// Large clock action button
const ClockButton = ({ label, icon: Icon, onClick, variant }) => {
const variants = {
primary: 'bg-coral-gradient text-white',
secondary: 'bg-white/10 text-white border border-white/20',
danger: 'bg-red-500/80 text-white'
}

return (
<button
onClick={onClick}
className={`
w-full py-4 px-6 rounded-xl font-semibold text-lg
flex items-center justify-center gap-3
active:scale-[0.98] transition-transform
min-h-[56px]
${variants[variant]}
`}
>
<Icon className="w-6 h-6" />
{label}
</button>
)
}

Leave Requests

Mobile Leave Form

// Mobile leave request form
const MobileLeaveForm = () => (
<div className="min-h-screen pb-20">
<header className="sticky top-0 bg-background/95 backdrop-blur-md z-10 p-4">
<div className="flex items-center gap-3">
<BackButton />
<h1 className="text-xl font-semibold">Request Leave</h1>
</div>
</header>

<form className="p-4 space-y-4">
{/* Leave Type */}
<div>
<label className="block text-sm font-medium mb-2">Leave Type</label>
<select className="form-select w-full py-3 text-base">
<option value="annual">Annual Leave</option>
<option value="sick">Sick Leave</option>
<option value="emergency">Emergency Leave</option>
</select>
</div>

{/* Date Range */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium mb-2">Start Date</label>
<input
type="date"
className="form-input w-full py-3 text-base"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">End Date</label>
<input
type="date"
className="form-input w-full py-3 text-base"
/>
</div>
</div>

{/* Half Day Options */}
<div className="grid grid-cols-2 gap-3">
<label className="flex items-center gap-3 p-3 bg-white/5 rounded-lg">
<input type="checkbox" className="w-5 h-5" />
<span>Half day start</span>
</label>
<label className="flex items-center gap-3 p-3 bg-white/5 rounded-lg">
<input type="checkbox" className="w-5 h-5" />
<span>Half day end</span>
</label>
</div>

{/* Reason */}
<div>
<label className="block text-sm font-medium mb-2">Reason</label>
<textarea
className="form-input w-full py-3 text-base min-h-[120px]"
placeholder="Please provide a reason for your leave request..."
/>
</div>

{/* Submit */}
<button className="form-button w-full py-4 text-lg mt-6">
Submit Request
</button>
</form>
</div>
)

Leave Balance Card

// Mobile leave balance display
const MobileLeaveBalance = ({ balance }) => (
<div className="card-glass p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold">Leave Balance</h3>
<span className="text-secondary-text text-sm">2025</span>
</div>

<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-green-400">
{balance.remaining}
</div>
<div className="text-xs text-secondary-text">Remaining</div>
</div>
<div>
<div className="text-2xl font-bold text-amber-400">
{balance.pending}
</div>
<div className="text-xs text-secondary-text">Pending</div>
</div>
<div>
<div className="text-2xl font-bold text-secondary-text">
{balance.used}
</div>
<div className="text-xs text-secondary-text">Used</div>
</div>
</div>
</div>
)

Touch Interactions

Touch Targets

All interactive elements meet the 44px minimum touch target:

ElementMinimum Size
Buttons44px × 44px
Links44px height
Form inputs48px height
Navigation items44px × 44px
List items48px height

Touch States

// Touch-friendly button with feedback
const TouchButton = ({ children, onClick, ...props }) => (
<button
onClick={onClick}
className={`
min-h-[44px] min-w-[44px] px-4
active:bg-white/5 active:scale-[0.98]
transition-all duration-150
`}
{...props}
>
{children}
</button>
)

Pull to Refresh

// Pull to refresh implementation
const usePullToRefresh = (onRefresh: () => Promise<void>) => {
const [isPulling, setIsPulling] = useState(false)
const [pullDistance, setPullDistance] = useState(0)
const [isRefreshing, setIsRefreshing] = useState(false)

const threshold = 80 // pixels to pull before refresh triggers

const handleTouchMove = (e: React.TouchEvent) => {
if (window.scrollY === 0) {
const distance = e.touches[0].clientY - startY
if (distance > 0) {
setIsPulling(true)
setPullDistance(Math.min(distance, threshold * 1.5))
}
}
}

const handleTouchEnd = async () => {
if (pullDistance >= threshold) {
setIsRefreshing(true)
await onRefresh()
setIsRefreshing(false)
}
setIsPulling(false)
setPullDistance(0)
}

return {
isPulling,
isRefreshing,
pullDistance,
handlers: { onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd }
}
}

Offline Support

Cached Data

When offline, you can still access:

FeatureAvailable Offline
View schedule✅ Cached shifts
View profile✅ Cached data
Clock in/out⚠️ Queued for sync
Request leave⚠️ Queued for sync
View balance✅ Last known

Offline Indicator

// Offline status indicator
const OfflineIndicator = () => {
const isOnline = useOnlineStatus()

if (isOnline) return null

return (
<div className="fixed top-0 left-0 right-0 bg-amber-600 text-white text-center py-2 text-sm z-50 safe-area-top">
You're offline. Some features may be limited.
</div>
)
}

// Hook to track online status
const useOnlineStatus = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine)

useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)

window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)

return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])

return isOnline
}

Data Synchronisation

// Sync queued actions when online
const syncQueuedActions = async () => {
const queue = getActionQueue()

for (const action of queue) {
try {
await executeAction(action)
removeFromQueue(action.id)
} catch (error) {
// Keep in queue for retry
console.error('Sync failed:', error)
}
}
}

// Listen for online event to sync
useEffect(() => {
const handleOnline = () => {
syncQueuedActions()
}

window.addEventListener('online', handleOnline)
return () => window.removeEventListener('online', handleOnline)
}, [])

Push Notifications

Enabling Notifications

  1. Open Shyfts on your mobile device
  2. Go to ProfilePreferences
  3. Toggle Push Notifications on
  4. Allow when prompted by browser

Notification Types

NotificationDescription
Shift ReminderBefore your shift starts
Schedule ChangeWhen your shift is modified
Leave UpdateRequest approved/rejected
Swap RequestNew swap request received

Request Permission

// Request push notification permission
const requestNotificationPermission = async () => {
if (!('Notification' in window)) {
return { supported: false }
}

if (Notification.permission === 'granted') {
return { supported: true, granted: true }
}

if (Notification.permission === 'denied') {
return { supported: true, granted: false, denied: true }
}

const permission = await Notification.requestPermission()
return { supported: true, granted: permission === 'granted' }
}

Form Input

Mobile-Optimised Forms

// Mobile form input with proper keyboard support
const MobileInput = ({ label, type, value, onChange, placeholder }) => (
<div>
<label className="block text-sm font-medium mb-2">{label}</label>
<input
type={type}
value={value}
onChange={onChange}
placeholder={placeholder}
className={`
form-input w-full py-3 text-base
focus:ring-2 focus:ring-coral-500
`}
// Prevent zoom on iOS
style={{ fontSize: '16px' }}
/>
</div>
)

Input Types for Mobile

Field TypeInput TypeKeyboard
Emailtype="email"Email keyboard
Phonetype="tel"Phone keyboard
Numbertype="number"Numeric keyboard
Datetype="date"Date picker
Timetype="time"Time picker
Searchtype="search"Search keyboard

Date Picker

// Mobile-friendly date picker
const MobileDatePicker = ({ label, value, onChange, min, max }) => (
<div>
<label className="block text-sm font-medium mb-2">{label}</label>
<input
type="date"
value={value}
onChange={(e) => onChange(e.target.value)}
min={min}
max={max}
className="form-input w-full py-3 text-base"
// Show native date picker on tap
/>
</div>
)

Performance

Mobile Optimisations

OptimisationBenefit
Lazy loadingFaster initial load
Image compressionReduced data usage
Code splittingSmaller bundles
Service workerOffline caching

Loading States

// Mobile loading skeleton
const MobileShiftSkeleton = () => (
<div className="card-glass p-4 animate-pulse">
<div className="h-5 bg-white/10 rounded w-1/2 mb-2" />
<div className="h-4 bg-white/10 rounded w-1/3" />
<div className="h-4 bg-white/10 rounded w-2/3 mt-3" />
</div>
)

// Loading state wrapper
const LoadingWrapper = ({ isLoading, children, skeleton }) => {
if (isLoading) {
return skeleton || <DefaultSkeleton />
}
return children
}

Accessibility

Mobile Accessibility

FeatureImplementation
Touch targets44px minimum
Colour contrast4.5:1 ratio
Font size16px minimum
Focus indicatorsVisible on tap
Screen readerARIA labels

VoiceOver/TalkBack Support

// Accessible button with ARIA
const AccessibleButton = ({ label, onClick, icon: Icon }) => (
<button
onClick={onClick}
aria-label={label}
className="min-h-[44px] min-w-[44px] p-3"
>
<Icon className="w-6 h-6" aria-hidden="true" />
<span className="sr-only">{label}</span>
</button>
)

// Accessible status announcement
const StatusAnnouncement = ({ message }) => (
<div
role="status"
aria-live="polite"
className="sr-only"
>
{message}
</div>
)

Troubleshooting

Common Issues

IssueCauseSolution
Won't add to home screenBrowser limitationTry different browser
Notifications not workingPermission deniedCheck browser settings
Slow loadingPoor connectionUse WiFi or wait
Form zoom on iOSFont size < 16pxAlready fixed in Shyfts
Gestures not workingTouch event issuesClear cache, restart browser

Clearing Cache

iPhone/iPad:

  1. Settings → Safari → Clear History and Website Data
  2. Or: Settings → Safari → Advanced → Website Data → Remove for Shyfts

Android:

  1. Chrome → Menu → Settings → Privacy → Clear Browsing Data
  2. Select "Cached images and files"
  3. Tap "Clear Data"

Best Practices

Mobile Usage Tips

  1. Add to home screen - For best experience
  2. Enable notifications - Stay informed
  3. Use WiFi - For initial setup
  4. Keep logged in - Persistent session
  5. Update browser - Latest features

Battery Considerations

FeatureBattery Impact
Location servicesMedium
Push notificationsLow
Background syncLow
Screen brightnessHigh


Source Files:

  • src/components/mobile/MobileNavigation.tsx - Bottom navigation
  • src/components/mobile/MobileShiftCard.tsx - Shift cards
  • src/hooks/useSwipeGesture.ts - Swipe detection
  • src/hooks/useOnlineStatus.ts - Network status
  • src/lib/pwa/service-worker.ts - Offline support