Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2803c0cfe4
commit
445d72c4f8
258
package-lock.json
generated
258
package-lock.json
generated
@ -5,8 +5,11 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "singsalot-ai",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.6.5",
|
||||
"@ionic/react": "^8.6.5",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@ -1221,6 +1224,32 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.5.tgz",
|
||||
"integrity": "sha512-HN+6/Q67fEEpRA86QzXSrCahuHwaTPBsa910RuvY0pIYuoY4rpzGPU9ZOQ5q2wBsrln921rroEPU1xdpPKIH8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.33.1",
|
||||
"ionicons": "^7.2.2",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/react": {
|
||||
"version": "8.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.5.tgz",
|
||||
"integrity": "sha512-reUKqlU3cIJoHuDibB8WUd32a7nqg5aMsIfPnXOVydIUsJdvQnwjACbYiP0g+4AFzTVPsw/Cmyqh85GhXGw4WA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.6.5",
|
||||
"ionicons": "^7.0.0",
|
||||
"tslib": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.6",
|
||||
"react-dom": ">=16.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
@ -1653,6 +1682,133 @@
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.33.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
|
||||
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "4.34.9",
|
||||
"@rollup/rollup-darwin-x64": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.9",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
|
||||
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
|
||||
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz",
|
||||
@ -3285,6 +3441,15 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -5336,6 +5501,26 @@
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.5.tgz",
|
||||
"integrity": "sha512-HN+6/Q67fEEpRA86QzXSrCahuHwaTPBsa910RuvY0pIYuoY4rpzGPU9ZOQ5q2wBsrln921rroEPU1xdpPKIH8Q==",
|
||||
"requires": {
|
||||
"@stencil/core": "4.33.1",
|
||||
"ionicons": "^7.2.2",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@ionic/react": {
|
||||
"version": "8.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.5.tgz",
|
||||
"integrity": "sha512-reUKqlU3cIJoHuDibB8WUd32a7nqg5aMsIfPnXOVydIUsJdvQnwjACbYiP0g+4AFzTVPsw/Cmyqh85GhXGw4WA==",
|
||||
"requires": {
|
||||
"@ionic/core": "8.6.5",
|
||||
"ionicons": "^7.0.0",
|
||||
"tslib": "*"
|
||||
}
|
||||
},
|
||||
"@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
@ -5621,6 +5806,71 @@
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "4.33.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
|
||||
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
|
||||
"requires": {
|
||||
"@rollup/rollup-darwin-arm64": "4.34.9",
|
||||
"@rollup/rollup-darwin-x64": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.9",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
|
||||
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-darwin-x64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
|
||||
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@swc/core": {
|
||||
"version": "1.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz",
|
||||
@ -6665,6 +6915,14 @@
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"requires": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.6.5",
|
||||
"@ionic/react": "^8.6.5",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonFooter, IonChip } from '@ionic/react';
|
||||
import { selectCurrentSinger, selectIsAdmin, selectControllerName } from '../../redux/authSlice';
|
||||
import { logout } from '../../redux/authSlice';
|
||||
import { ActionButton } from '../common';
|
||||
@ -18,63 +19,55 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo/Title */}
|
||||
<IonApp>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
🎤 Karaoke App
|
||||
</h1>
|
||||
<span>🎤 Karaoke App</span>
|
||||
{controllerName && (
|
||||
<span className="ml-4 text-sm text-gray-500">
|
||||
Party: {controllerName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Info & Logout */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{currentSinger && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">{currentSinger}</span>
|
||||
{isAdmin && (
|
||||
<span className="ml-2 px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={handleLogout}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Logout
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</IonTitle>
|
||||
|
||||
{/* User Info & Logout */}
|
||||
{currentSinger && (
|
||||
<div slot="end" className="flex items-center space-x-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">{currentSinger}</span>
|
||||
{isAdmin && (
|
||||
<IonChip color="primary">
|
||||
Admin
|
||||
</IonChip>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={handleLogout}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Logout
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1">
|
||||
<IonContent>
|
||||
{children}
|
||||
</main>
|
||||
</IonContent>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-gray-200 mt-auto">
|
||||
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||
<IonFooter>
|
||||
<IonToolbar>
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
<p>🎵 Powered by Firebase Realtime Database</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</IonToolbar>
|
||||
</IonFooter>
|
||||
</IonApp>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,42 +1,80 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { IonTabs, IonTabBar, IonTabButton, IonLabel, IonIcon } from '@ionic/react';
|
||||
import { list, search, heart, add, mic, documentText, time, trophy, people } from 'ionicons/icons';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
const Navigation: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const navItems = [
|
||||
{ path: '/queue', label: 'Queue', icon: '📋' },
|
||||
{ path: '/search', label: 'Search', icon: '🔍' },
|
||||
{ path: '/favorites', label: 'Favorites', icon: '❤️' },
|
||||
{ path: '/new-songs', label: 'New Songs', icon: '🆕' },
|
||||
{ path: '/artists', label: 'Artists', icon: '🎤' },
|
||||
{ path: '/song-lists', label: 'Song Lists', icon: '📝' },
|
||||
{ path: '/history', label: 'History', icon: '⏰' },
|
||||
{ path: '/top-played', label: 'Top 100', icon: '🏆' },
|
||||
{ path: '/singers', label: 'Singers', icon: '👥' },
|
||||
{ path: '/queue', label: 'Queue', icon: list },
|
||||
{ path: '/search', label: 'Search', icon: search },
|
||||
{ path: '/favorites', label: 'Favorites', icon: heart },
|
||||
{ path: '/new-songs', label: 'New Songs', icon: add },
|
||||
{ path: '/artists', label: 'Artists', icon: mic },
|
||||
{ path: '/song-lists', label: 'Song Lists', icon: documentText },
|
||||
{ path: '/history', label: 'History', icon: time },
|
||||
{ path: '/top-played', label: 'Top 100', icon: trophy },
|
||||
{ path: '/singers', label: 'Singers', icon: people },
|
||||
];
|
||||
|
||||
// For mobile, show bottom tabs with main features
|
||||
const mobileNavItems = [
|
||||
{ path: '/queue', label: 'Queue', icon: list },
|
||||
{ path: '/search', label: 'Search', icon: search },
|
||||
{ path: '/favorites', label: 'Favorites', icon: heart },
|
||||
{ path: '/history', label: 'History', icon: time },
|
||||
];
|
||||
|
||||
// Check if we're on mobile (you can adjust this breakpoint)
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
const currentItems = isMobile ? mobileNavItems : navItems;
|
||||
|
||||
return (
|
||||
<nav className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex space-x-8">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) => `
|
||||
flex items-center px-3 py-4 text-sm font-medium border-b-2 transition-colors
|
||||
${isActive
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="mr-2">{item.icon}</span>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<>
|
||||
{isMobile ? (
|
||||
<IonTabs>
|
||||
<IonTabBar slot="bottom">
|
||||
{currentItems.map((item) => (
|
||||
<IonTabButton
|
||||
key={item.path}
|
||||
tab={item.path}
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
>
|
||||
<IonIcon icon={item.icon} />
|
||||
<IonLabel>{item.label}</IonLabel>
|
||||
</IonTabButton>
|
||||
))}
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
) : (
|
||||
<nav className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex space-x-8">
|
||||
{currentItems.map((item) => (
|
||||
<button
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className={`
|
||||
flex items-center px-3 py-4 text-sm font-medium border-b-2 transition-colors
|
||||
${location.pathname === item.path
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IonIcon icon={item.icon} className="mr-2" />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IonButton } from '@ionic/react';
|
||||
import type { ActionButtonProps } from '../../types';
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
@ -9,46 +10,43 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
disabled = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const getVariantStyles = () => {
|
||||
const getVariant = () => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
||||
return 'primary';
|
||||
case 'secondary':
|
||||
return 'bg-gray-200 hover:bg-gray-300 text-gray-800';
|
||||
return 'medium';
|
||||
case 'danger':
|
||||
return 'bg-red-600 hover:bg-red-700 text-white';
|
||||
return 'danger';
|
||||
default:
|
||||
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
||||
return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
const getSizeStyles = () => {
|
||||
const getSize = () => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'px-2 py-1 text-xs';
|
||||
return 'small';
|
||||
case 'md':
|
||||
return 'px-3 py-2 text-sm';
|
||||
return 'default';
|
||||
case 'lg':
|
||||
return 'px-4 py-2 text-base';
|
||||
return 'large';
|
||||
default:
|
||||
return 'px-3 py-2 text-sm';
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
<IonButton
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`
|
||||
font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
${getVariantStyles()}
|
||||
${getSizeStyles()}
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
|
||||
${className}
|
||||
`}
|
||||
fill="solid"
|
||||
color={getVariant()}
|
||||
size={getSize()}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</IonButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IonContent } from '@ionic/react';
|
||||
import type { EmptyStateProps } from '../../types';
|
||||
|
||||
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
@ -8,26 +9,28 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
action
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
{icon && (
|
||||
<div className="mb-4 text-gray-400">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
{message && (
|
||||
<p className="text-sm text-gray-500 mb-4 max-w-sm">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
{action && (
|
||||
<div className="mt-4">
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<IonContent className="ion-padding">
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
{icon && (
|
||||
<div className="mb-4 text-gray-400">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
{message && (
|
||||
<p className="text-sm text-gray-500 mb-4 max-w-sm">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
{action && (
|
||||
<div className="mt-4">
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IonCard, IonCardContent, IonChip, IonIcon } from '@ionic/react';
|
||||
import { play, pause, stop } from 'ionicons/icons';
|
||||
import ActionButton from './ActionButton';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectPlayerState, selectIsAdmin, selectQueue } from '../../redux';
|
||||
@ -70,63 +72,73 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({ className = '' }) => {
|
||||
console.log('PlayerControls - currentState:', currentState);
|
||||
console.log('PlayerControls - hasSongsInQueue:', hasSongsInQueue);
|
||||
|
||||
const getStateColor = () => {
|
||||
switch (currentState) {
|
||||
case PlayerState.playing:
|
||||
return 'success';
|
||||
case PlayerState.paused:
|
||||
return 'warning';
|
||||
default:
|
||||
return 'medium';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-lg shadow p-4 ${className}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-lg font-medium text-gray-900">Player Controls</h3>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
currentState === PlayerState.playing
|
||||
? 'bg-green-100 text-green-800'
|
||||
: currentState === PlayerState.paused
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{currentState}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center space-x-3">
|
||||
{currentState === PlayerState.playing ? (
|
||||
<ActionButton
|
||||
onClick={handlePause}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
>
|
||||
⏸️ Pause
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
onClick={handlePlay}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
disabled={!hasSongsInQueue}
|
||||
>
|
||||
▶️ Play
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
{currentState !== PlayerState.stopped && (
|
||||
<ActionButton
|
||||
onClick={handleStop}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
⏹️ Stop
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-500 text-center">
|
||||
Admin controls - Only visible to admin users
|
||||
{!hasSongsInQueue && (
|
||||
<div className="mt-1 text-orange-600">
|
||||
Add songs to queue to enable playback controls
|
||||
<IonCard className={className}>
|
||||
<IonCardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-lg font-medium text-gray-900">Player Controls</h3>
|
||||
<IonChip color={getStateColor()}>
|
||||
{currentState}
|
||||
</IonChip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center space-x-3">
|
||||
{currentState === PlayerState.playing ? (
|
||||
<ActionButton
|
||||
onClick={handlePause}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
>
|
||||
<IonIcon icon={pause} slot="start" />
|
||||
Pause
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
onClick={handlePlay}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
disabled={!hasSongsInQueue}
|
||||
>
|
||||
<IonIcon icon={play} slot="start" />
|
||||
Play
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
{currentState !== PlayerState.stopped && (
|
||||
<ActionButton
|
||||
onClick={handleStop}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={stop} slot="start" />
|
||||
Stop
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-500 text-center">
|
||||
Admin controls - Only visible to admin users
|
||||
{!hasSongsInQueue && (
|
||||
<div className="mt-1 text-orange-600">
|
||||
Add songs to queue to enable playback controls
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IonItem, IonLabel } from '@ionic/react';
|
||||
import ActionButton from './ActionButton';
|
||||
import type { SongItemProps } from '../../types';
|
||||
|
||||
@ -132,11 +133,8 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center justify-between p-4 border-b border-gray-200 hover:bg-gray-50 transition-colors
|
||||
${className}
|
||||
`}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<IonItem className={className}>
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||
{song.title}
|
||||
</h3>
|
||||
@ -154,12 +152,12 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
Played {song.count} times
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</IonLabel>
|
||||
|
||||
<div className="ml-4 flex-shrink-0">
|
||||
<div slot="end" className="flex gap-2">
|
||||
{renderActionPanel()}
|
||||
</div>
|
||||
</div>
|
||||
</IonItem>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { IonToast } from '@ionic/react';
|
||||
import type { ToastProps } from '../../types';
|
||||
|
||||
const Toast: React.FC<ToastProps> = ({
|
||||
@ -7,50 +8,41 @@ const Toast: React.FC<ToastProps> = ({
|
||||
duration = 3000,
|
||||
onClose
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300); // Wait for fade out animation
|
||||
}, duration);
|
||||
const handleDismiss = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [duration, onClose]);
|
||||
|
||||
const getTypeStyles = () => {
|
||||
const getColor = () => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'bg-green-500 text-white';
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'bg-red-500 text-white';
|
||||
return 'danger';
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-blue-500 text-white';
|
||||
return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
fixed top-4 right-4 z-50 px-4 py-2 rounded-md shadow-lg transition-opacity duration-300
|
||||
${getTypeStyles()}
|
||||
${isVisible ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm font-medium">{message}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300);
|
||||
}}
|
||||
className="ml-3 text-white hover:text-gray-200 focus:outline-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<IonToast
|
||||
isOpen={isOpen}
|
||||
onDidDismiss={handleDismiss}
|
||||
message={message}
|
||||
duration={duration}
|
||||
color={getColor()}
|
||||
position="top"
|
||||
buttons={[
|
||||
{
|
||||
text: '×',
|
||||
role: 'cancel',
|
||||
handler: handleDismiss
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SongItem, EmptyState, ActionButton, PlayerControls } from '../../components/common';
|
||||
import { IonList, IonItem, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonLabel, IonChip } from '@ionic/react';
|
||||
import { trash, arrowUp, arrowDown } from 'ionicons/icons';
|
||||
import { EmptyState, ActionButton, PlayerControls } from '../../components/common';
|
||||
import { useQueue } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectQueue, selectPlayerState } from '../../redux';
|
||||
@ -11,7 +13,6 @@ const Queue: React.FC = () => {
|
||||
queueStats,
|
||||
canReorder,
|
||||
handleRemoveFromQueue,
|
||||
handleToggleFavorite,
|
||||
handleMoveUp,
|
||||
handleMoveDown,
|
||||
} = useQueue();
|
||||
@ -73,67 +74,79 @@ const Queue: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
<IonList>
|
||||
{queueItems.map((queueItem, index) => {
|
||||
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
|
||||
const canDelete = index === 0 ? canDeleteFirstItem : true;
|
||||
|
||||
return (
|
||||
<div key={queueItem.key} className="flex items-center">
|
||||
{/* Order Number */}
|
||||
<div className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium">
|
||||
{queueItem.order}
|
||||
</div>
|
||||
<IonItemSliding key={queueItem.key}>
|
||||
<IonItem>
|
||||
{/* Order Number */}
|
||||
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium rounded-full">
|
||||
{queueItem.order}
|
||||
</div>
|
||||
|
||||
{/* Song Info */}
|
||||
<div className="flex-1">
|
||||
<SongItem
|
||||
song={queueItem.song}
|
||||
context="queue"
|
||||
onRemoveFromQueue={
|
||||
// Only allow removal of first item when stopped or paused
|
||||
index === 0 && !canDeleteFirstItem
|
||||
? undefined
|
||||
: () => handleRemoveFromQueue(queueItem)
|
||||
}
|
||||
onToggleFavorite={() => handleToggleFavorite(queueItem.song)}
|
||||
isAdmin={canReorder}
|
||||
/>
|
||||
</div>
|
||||
{/* Song Info */}
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||
{queueItem.song.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{queueItem.song.artist}
|
||||
</p>
|
||||
<div className="flex items-center mt-1">
|
||||
<IonChip color="medium">
|
||||
{queueItem.singer.name}
|
||||
</IonChip>
|
||||
{queueItem.isCurrentUser && (
|
||||
<IonChip color="primary">
|
||||
You
|
||||
</IonChip>
|
||||
)}
|
||||
</div>
|
||||
</IonLabel>
|
||||
|
||||
{/* Singer Info */}
|
||||
<div className="flex-shrink-0 px-4 py-2 text-sm text-gray-600">
|
||||
<div className="font-medium">{queueItem.singer.name}</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{queueItem.isCurrentUser ? '(You)' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Controls */}
|
||||
{canReorder && (
|
||||
<div className="flex-shrink-0 px-4 py-2 flex flex-col gap-1">
|
||||
{queueItem.order > 2 && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveUp(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
↑
|
||||
</ActionButton>
|
||||
{/* Admin Controls */}
|
||||
{canReorder && (
|
||||
<div slot="end" className="flex flex-col gap-1">
|
||||
{queueItem.order > 2 && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveUp(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={arrowUp} />
|
||||
</ActionButton>
|
||||
)}
|
||||
{queueItem.order > 1 && queueItem.order < queueItems.length && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveDown(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={arrowDown} />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{queueItem.order > 1 && queueItem.order < queueItems.length && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveDown(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
</IonItem>
|
||||
|
||||
{/* Swipe Actions */}
|
||||
{canDelete && (
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption
|
||||
color="danger"
|
||||
onClick={() => handleRemoveFromQueue(queueItem)}
|
||||
>
|
||||
↓
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
<IonIcon icon={trash} slot="icon-only" />
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
)}
|
||||
</IonItemSliding>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</IonList>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,17 @@
|
||||
/* Ionic CSS imports */
|
||||
@import '@ionic/react/css/core.css';
|
||||
@import '@ionic/react/css/normalize.css';
|
||||
@import '@ionic/react/css/structure.css';
|
||||
@import '@ionic/react/css/typography.css';
|
||||
@import '@ionic/react/css/display.css';
|
||||
@import '@ionic/react/css/padding.css';
|
||||
@import '@ionic/react/css/float-elements.css';
|
||||
@import '@ionic/react/css/text-alignment.css';
|
||||
@import '@ionic/react/css/text-transformation.css';
|
||||
@import '@ionic/react/css/flex-utils.css';
|
||||
@import '@ionic/react/css/display.css';
|
||||
|
||||
/* Tailwind CSS */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { setupIonicReact } from '@ionic/react'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './redux/store';
|
||||
|
||||
// Initialize Ionic React
|
||||
setupIonicReact();
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user