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 Profile → Preferences
  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