@@ -0,0 +1,2 @@
|
||||
#Wed Jan 13 17:35:11 CST 2021
|
||||
gradle.version=5.6.4
|
||||
@@ -1,14 +1,56 @@
|
||||
# Next.js + Tailwind CSS + Capacitor for mobile app development
|
||||
# Next.js + Tailwind CSS + Ionic Framework + Capacitor Mobile Starter
|
||||
|
||||
*Note: this repo is in active development and not quite ready for production use!*
|
||||

|
||||
|
||||
This repo is a starting point for building an iOS, Android, and Progressive Web App with Tailwind CSS, Next.js, and Capacitor. It comes with some pre-built components that can be customized using Tailwind classes, and provides the most important UI controls needed to build native mobile experiences (tabs, nav bars, modals, menus, etc).
|
||||
_Note: this repo is in active development and not quite ready for production use!_
|
||||
|
||||
These components are baked into the starter and will be adopted into your project. This way you gain full control over the experience and can easily modify the look and feel of the components to match your design.
|
||||
This repo is a starting point for building an iOS, Android, and Progressive Web App with Next.js, Tailwind CSS, Ionic Framework, and Capacitor.
|
||||
|
||||
If you're looking for more of a batteries-included approach where you _don't_ adopt and maintain the components yourself, I recommend [Ionic React](https://ionicframework.com/react).
|
||||
Next.js handles the production React app experience, Tailwind can be used to style each page of your app, Ionic Framework provides the cross-platform system controls (navigation/transitions/tabs/etc.), and then Capacitor bundles all of it up and runs it on iOS, Android, and Web with full native access.
|
||||
|
||||

|
||||
## Usage
|
||||
|
||||
This project is a standard Next.js app, so the typical Next.js development process applies (`npm run dev` for browser-based development). However, there is one caveat: the app must be exported to deploy to iOS and Android, since it must run purely client-side. ([more on Next.js export](https://nextjs.org/docs/advanced-features/static-html-export))
|
||||
|
||||
To build the app, run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run export
|
||||
```
|
||||
|
||||
All the client side files will be sent to the `./out/` directory. These files need to be copied to the native iOS and Android projects, and this is where Capacitor comes in:
|
||||
|
||||
```bash
|
||||
npx cap copy
|
||||
```
|
||||
|
||||
Finally, to run the app, open the Native IDE for the platform and follow the IDE's run process (note: a CLI run [will be available in Capacitor 3](https://capacitorjs.com/blog/announcing-capacitor-3-0-beta):
|
||||
|
||||
```
|
||||
npx cap open ios
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
## Livereload/Instant Refresh
|
||||
|
||||
To enable Livereload and Instant Refresh during development (when running `npm run dev`), find the IP address of your local interface (ex: `192.168.1.2`) and port your Next.js server is running on, and then set the server url config value to point to it in `capacitor.config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"url": "192.168.1.2:3000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: this configuration wil be easier in Capacitor 3 which [recently went into beta](https://capacitorjs.com/blog/announcing-capacitor-3-0-beta).
|
||||
|
||||
## Caveats
|
||||
|
||||
One caveat with this project: Because the app must be able to run purely client-side and use [Next.js's Export command](https://nextjs.org/docs/advanced-features/static-html-export), that means no Server Side Rendering in this code base. There is likely a way to SSR and a fully static Next.js app in tandem but it requires [a Babel plugin](https://github.com/erzr/next-babel-conditional-ssg-ssr) or would involve a more elaborate monorepo setup with code sharing that is out of scope for this project.
|
||||
|
||||
Additionally, Next.js routing is not really used much in this app beyond a catch-all route to render the native app shell and engage the Ionic React Router. This is primarily because Next.js routing is not set up to enable native-style transitions and history state management like the kind Ionic uses.
|
||||
|
||||
## What is Capacitor?
|
||||
|
||||
@@ -17,20 +59,3 @@ You can think of [Capacitor](https://capacitorjs.com/) as a sort of "electron fo
|
||||
Capacitor provides access to Native APIs and a plugin system for building any native functionality your app needs.
|
||||
|
||||
Capacitor apps can also run in the browser as a Progressive Web App with the same code.
|
||||
|
||||
## Progress
|
||||
|
||||
There are currently snippets for the following common mobile components:
|
||||
|
||||
- [x] App Shell
|
||||
- [x] Content
|
||||
- [x] Tabs
|
||||
- [ ] Nav (in progress)
|
||||
- [ ] Next.js router integration
|
||||
- [x] Icon
|
||||
- [x] Menu
|
||||
- [x] Modal
|
||||
- [ ] Dialog
|
||||
- [x] Button
|
||||
- [x] Card
|
||||
- [x] Safe Area
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# NPM renames .gitignore to .npmignore
|
||||
# In order to prevent that, we remove the initial "."
|
||||
# And the CLI then renames it
|
||||
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -0,0 +1,51 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.example.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-dark-mode')
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.app">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name="com.example.app.MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="@string/custom_url_scheme" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Camera, Photos, input file -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<!-- Geolocation API -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
<!-- Network API -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Navigator.getUserMedia -->
|
||||
<!-- Video -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- Audio -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"appId": "com.example.app",
|
||||
"appName": "nextjs-tailwind-capacitor",
|
||||
"bundledWebRuntime": false,
|
||||
"npmClient": "npm",
|
||||
"webDir": "out",
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 0
|
||||
}
|
||||
},
|
||||
"cordova": {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.app;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import com.getcapacitor.Plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initializes the Bridge
|
||||
this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
|
||||
// Additional plugins you've installed go here
|
||||
// Ex: add(TotallyAwesomePlugin.class);
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">nextjs-tailwind-capacitor</string>
|
||||
<string name="title_activity_main">nextjs-tailwind-capacitor</string>
|
||||
<string name="package_name">com.example.app</string>
|
||||
<string name="custom_url_scheme">com.example.app</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
|
||||
</widget>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.1'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "variables.gradle"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-dark-mode'
|
||||
project(':capacitor-dark-mode').projectDir = new File('../node_modules/capacitor-dark-mode/android')
|
||||
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,100 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,5 @@
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
@@ -0,0 +1,17 @@
|
||||
ext {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
androidxAppCompatVersion = '1.1.0'
|
||||
androidxCoreVersion = '1.2.0'
|
||||
androidxMaterialVersion = '1.1.0-rc02'
|
||||
androidxBrowserVersion = '1.2.0'
|
||||
androidxLocalbroadcastmanagerVersion = '1.0.0'
|
||||
androidxExifInterfaceVersion = '1.2.0'
|
||||
firebaseMessagingVersion = '20.1.2'
|
||||
playServicesLocationVersion = '17.0.0'
|
||||
junitVersion = '4.12'
|
||||
androidxJunitVersion = '1.1.1'
|
||||
androidxEspressoCoreVersion = '3.2.0'
|
||||
cordovaAndroidVersion = '7.0.0'
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react';
|
||||
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import Menu from './Menu';
|
||||
|
||||
import Tabs from './pages/Tabs';
|
||||
|
||||
const AppShell = () => {
|
||||
return (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonSplitPane contentId="main">
|
||||
<Menu />
|
||||
<IonRouterOutlet id="main">
|
||||
<Route path="/tabs" render={() => <Tabs />} />
|
||||
<Route exact path="/" render={() => <Redirect to="/tabs" />} />
|
||||
</IonRouterOutlet>
|
||||
</IonSplitPane>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppShell;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Plugins, StatusBarStyle } from '@capacitor/core';
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonRouterContext,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { cog, flash, list } from 'ionicons/icons';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'Feed',
|
||||
icon: flash,
|
||||
url: '/tabs/feed',
|
||||
},
|
||||
{
|
||||
title: 'Lists',
|
||||
icon: list,
|
||||
url: '/tabs/lists',
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
icon: cog,
|
||||
url: '/tabs/settings',
|
||||
},
|
||||
];
|
||||
|
||||
const Menu = () => {
|
||||
const { StatusBar } = Plugins;
|
||||
|
||||
const ionRouterContext = useContext(IonRouterContext);
|
||||
const location = useLocation();
|
||||
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
const handleOpen = async () => {
|
||||
try {
|
||||
await StatusBar.setStyle({
|
||||
style: isDark ? StatusBarStyle.Light : StatusBarStyle.Dark,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
await StatusBar.setStyle({
|
||||
style: isDark ? StatusBarStyle.Dark : StatusBarStyle.Light,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IonMenu side="start" contentId="main" onIonDidOpen={handleOpen} onIonDidClose={handleClose}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Menu</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<IonList>
|
||||
{pages.map(p => (
|
||||
<IonMenuToggle autoHide={false}>
|
||||
<IonItem routerLink={p.url} routerDirection="none" detail={false} lines="none">
|
||||
<IonIcon icon={p.icon} slot="start" />
|
||||
<IonLabel>{p.title}</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
))}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -1,44 +0,0 @@
|
||||
import Store from '../store';
|
||||
import * as actions from '../store/actions';
|
||||
import * as selectors from '../store/selectors';
|
||||
|
||||
const MenuItem = ({ children, ...props }) => (
|
||||
<li {...props}>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-800 hover:text-gray-400 dark:text-gray-400 block px-4 py-2 rounded-md text-base font-medium"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
const MenuContent = () => {
|
||||
const menuLinks = Store.useState(selectors.getMenuLinks);
|
||||
|
||||
const go = page => {
|
||||
actions.setPage(page);
|
||||
actions.setMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl select-none dark:text-gray-500">Menu</h2>
|
||||
</div>
|
||||
<ul>
|
||||
{menuLinks.map(p => {
|
||||
const title = typeof p.title === 'function' ? p.title() : p.title;
|
||||
|
||||
return (
|
||||
<MenuItem key={p.id} onClick={() => go(p)}>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuContent;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { checkmarkOutline, closeOutline } from 'ionicons/icons';
|
||||
import Button from './ui/Button';
|
||||
import Icon from './ui/Icon';
|
||||
import List from './ui/List';
|
||||
import ListItem from './ui/ListItem';
|
||||
import VirtualScroll from './ui/VirtualScroll';
|
||||
|
||||
const NotificationItem = ({ i }) => (
|
||||
<ListItem className="flex align-center dark:bg-black">
|
||||
<img
|
||||
src={`/img/faces/image-${(i % 66) + 1}.png`}
|
||||
alt="Notification"
|
||||
className="block rounded-full w-8 h-8 mr-4"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="p-0 m-0 align-middle dark:text-gray-500">You have a new friend request</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button className="background-transparent px-1 py-1 text-green-400 text-lg">
|
||||
<Icon icon={checkmarkOutline} />
|
||||
</Button>
|
||||
<Button className="background-transparent px-1 py-1 text-red-400 text-lg">
|
||||
<Icon icon={closeOutline} />
|
||||
</Button>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const Notifications = () => (
|
||||
<div className="w-full h-full flex flex-col dark:bg-black">
|
||||
<div className="p-4 rounded-tl-md rounded-tr-md dark:bg-black">
|
||||
<h2 className="text-xl dark:text-gray-600">Notifications</h2>
|
||||
</div>
|
||||
<List className="flex-1">
|
||||
<VirtualScroll
|
||||
totalCount={1000}
|
||||
overscan={200}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
itemContent={index => <NotificationItem i={index} />}
|
||||
/>
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Notifications;
|
||||
@@ -0,0 +1,71 @@
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonContent,
|
||||
IonMenuButton,
|
||||
} from '@ionic/react';
|
||||
import Notifications from './Notifications';
|
||||
import { useState } from 'react';
|
||||
import { notificationsOutline } from 'ionicons/icons';
|
||||
import { getHomeItems } from '../../store/selectors';
|
||||
import Store from '../../store';
|
||||
|
||||
const FeedCard = ({ title, type, text, author, authorAvatar, image }) => (
|
||||
<Card className="my-4 mx-auto">
|
||||
<div>
|
||||
<img className="rounded-t-xl h-32 w-full object-cover" src={image} />
|
||||
</div>
|
||||
<div className="px-4 py-4 bg-white rounded-b-xl dark:bg-gray-900">
|
||||
<h4 className="font-bold py-0 text-s text-gray-400 dark:text-gray-500 uppercase">{type}</h4>
|
||||
<h2 className="font-bold text-2xl text-gray-800 dark:text-gray-100">{title}</h2>
|
||||
<p className="sm:text-sm text-s text-gray-500 mr-1 my-3 dark:text-gray-400">{text}</p>
|
||||
<div className="flex items-center space-x-4">
|
||||
<img src={authorAvatar} className="rounded-full w-10 h-10" />
|
||||
<h3 className="text-gray-500 dark:text-gray-200 m-l-8 text-sm font-medium">{author}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const Feed = () => {
|
||||
const homeItems = Store.useState(getHomeItems);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Feed</IonTitle>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => setShowNotifications(true)}>
|
||||
<IonIcon icon={notificationsOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent className="ion-padding" fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Feed</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<Notifications open={showNotifications} onDidDismiss={() => setShowNotifications(false)} />
|
||||
{homeItems.map((i, index) => (
|
||||
<FeedCard {...i} key={index} />
|
||||
))}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Feed;
|
||||
@@ -1,32 +0,0 @@
|
||||
import Store from '../../store';
|
||||
import Card from '../ui/Card';
|
||||
import Content from '../ui/Content';
|
||||
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
const HomeCard = ({ title, type, text, author, image }) => (
|
||||
<Card className="my-4">
|
||||
<div>
|
||||
<img className="rounded-t-xl h-32 w-full object-cover" src={image} />
|
||||
</div>
|
||||
<div className="px-4 py-4 bg-white rounded-b-xl dark:bg-gray-900">
|
||||
<h4 className="font-bold py-0 text-s text-gray-400 dark:text-gray-500 uppercase">{type}</h4>
|
||||
<h2 className="font-bold text-2xl text-gray-800 dark:text-gray-100">{title}</h2>
|
||||
<p className="sm:text-sm text-s text-gray-500 mr-1 my-3 dark:text-gray-400">{text}</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const Home = ({ selected }) => {
|
||||
const homeItems = Store.useState(selectors.getHomeItems);
|
||||
|
||||
return (
|
||||
<Content visible={selected} className="p-4 dark:bg-black">
|
||||
{homeItems.map((i, index) => (
|
||||
<HomeCard {...i} key={index} />
|
||||
))}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,61 +1,64 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonButtons,
|
||||
IonCheckbox,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
|
||||
import Store from '../../store';
|
||||
import * as actions from '../../store/actions';
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
import Content from '../ui/Content';
|
||||
import List from '../ui/List';
|
||||
import VirtualScroll from '../ui/VirtualScroll';
|
||||
|
||||
const ListItems = ({ list, onClose }) => {
|
||||
const ListItems = ({ list }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="py-2">
|
||||
<a href="#" onClick={onClose}>
|
||||
All Lists
|
||||
</a>
|
||||
</div>
|
||||
<VirtualScroll
|
||||
data={list?.items || []}
|
||||
totalCount={(list?.items || []).length}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
itemContent={(i, item) => <ListItemEntry list={list} item={item} />}
|
||||
/>
|
||||
</>
|
||||
<IonList>
|
||||
{(list?.items || []).map(item => (
|
||||
<ListItemEntry list={list} item={item} />
|
||||
))}
|
||||
</IonList>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItemEntry = ({ list, item }) => (
|
||||
<div
|
||||
className="p-4 border-solid border-b cursor-pointer flex select-none"
|
||||
onClick={() => actions.setDone(list, item, !item.done)}
|
||||
>
|
||||
<span className="text-md flex-1">{item.name}</span>
|
||||
<input
|
||||
className="pointer-events-none select-none"
|
||||
type="checkbox"
|
||||
checked={item.done || false}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
<IonItem onClick={() => actions.setDone(list, item, !item.done)}>
|
||||
<IonLabel>{item.name}</IonLabel>
|
||||
<IonCheckbox checked={item.done || false} slot="end" />
|
||||
</IonItem>
|
||||
);
|
||||
|
||||
const ListDetail = ({ selected }) => {
|
||||
const selectedList = Store.useState(selectors.getSelectedList);
|
||||
const ListDetail = ({ match }) => {
|
||||
const lists = Store.useState(selectors.getLists);
|
||||
const {
|
||||
params: { listId },
|
||||
} = match;
|
||||
const loadedList = lists.find(l => l.id === listId);
|
||||
|
||||
return (
|
||||
<Content visible={selected} className="p-4">
|
||||
<List className="h-full w-full">
|
||||
{selected && (
|
||||
<ListItems
|
||||
list={selectedList}
|
||||
onClose={() => {
|
||||
actions.setSelectedList(null);
|
||||
actions.setPageById('lists');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</Content>
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/tabs/lists" />
|
||||
</IonButtons>
|
||||
<IonTitle>{loadedList.name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">{loadedList.name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ListItems list={loadedList} />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
import Store from '../../store';
|
||||
import * as actions from '../../store/actions';
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
import Content from '../ui/Content';
|
||||
import List from '../ui/List';
|
||||
import VirtualScroll from '../ui/VirtualScroll';
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
} from '@ionic/react';
|
||||
|
||||
const ListEntry = ({ list, ...props }) => (
|
||||
<div {...props} className="p-4 border-solid dark:border-gray-800 border-b cursor-pointer dark:text-gray-200">
|
||||
<span className="text-md">{list.name}</span>
|
||||
</div>
|
||||
<IonItem href={`/tabs/lists/${list.id}`} className="list-entry">
|
||||
<IonLabel>{list.name}</IonLabel>
|
||||
</IonItem>
|
||||
);
|
||||
|
||||
const AllLists = ({ onSelect }) => {
|
||||
const lists = Store.useState(selectors.getLists);
|
||||
|
||||
return (
|
||||
<VirtualScroll
|
||||
data={lists}
|
||||
totalCount={lists.length}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
itemContent={(i, list) => (
|
||||
<ListEntry list={list} onClick={() => onSelect(list)} onClose={() => onSelect(null)} />
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
{lists.map((list, i) => (
|
||||
<ListEntry list={list} key={i} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Lists = ({ selected }) => {
|
||||
const Lists = () => {
|
||||
return (
|
||||
<Content visible={selected} className="p-4 dark:bg-black">
|
||||
<List className="h-full w-full">
|
||||
{selected && (
|
||||
<AllLists
|
||||
onSelect={list => {
|
||||
actions.setSelectedList(list);
|
||||
actions.setPageById('list-detail');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</Content>
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Lists</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Lists</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<AllLists />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
IonModal,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonNote,
|
||||
IonLabel,
|
||||
} from '@ionic/react';
|
||||
import Store from '../../store';
|
||||
import { getNotifications } from '../../store/selectors';
|
||||
|
||||
import { close } from 'ionicons/icons';
|
||||
|
||||
const NotificationItem = ({ notification }) => (
|
||||
<IonItem>
|
||||
<IonLabel>{notification.title}</IonLabel>
|
||||
<IonNote slot="end">{notification.when}</IonNote>
|
||||
<IonButton slot="end" fill="clear" color="dark">
|
||||
<IonIcon icon={close} />
|
||||
</IonButton>
|
||||
</IonItem>
|
||||
);
|
||||
|
||||
const Notifications = ({ open, onDidDismiss }) => {
|
||||
const notifications = Store.useState(getNotifications);
|
||||
|
||||
return (
|
||||
<IonModal isOpen={open} onDidDismiss={onDidDismiss}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Notifications</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Notifications</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonList>
|
||||
{notifications.map((notification, i) => (
|
||||
<NotificationItem notification={notification} key={i} />
|
||||
))}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
@@ -1,33 +1,46 @@
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonList,
|
||||
IonToggle,
|
||||
IonLabel,
|
||||
} from '@ionic/react';
|
||||
|
||||
import Store from '../../store';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import * as actions from '../../store/actions';
|
||||
import { setSettings } from '../../store/actions';
|
||||
|
||||
import Content from '../ui/Content';
|
||||
import List from '../ui/List';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Toggle from '../ui/Toggle';
|
||||
|
||||
const Settings = ({ selected }) => {
|
||||
const enableNotifications = Store.useState();
|
||||
const Settings = () => {
|
||||
const settings = Store.useState(selectors.getSettings);
|
||||
|
||||
return (
|
||||
<Content visible={selected} className="p-4 dark:bg-black">
|
||||
<List>
|
||||
<ListItem className="flex">
|
||||
<span className="text-md flex-1 dark:text-gray-200">Enable Notifications</span>
|
||||
<Toggle
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Settings</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<IonList>
|
||||
<IonItem>
|
||||
<IonLabel>Enable Notifications</IonLabel>
|
||||
<IonToggle
|
||||
checked={settings.enableNotifications}
|
||||
onChange={e =>
|
||||
actions.setSettings({
|
||||
onIonChange={e => {
|
||||
setSettings({
|
||||
...settings,
|
||||
enableNotifications: e.target.checked,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Content>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import { IonRouterOutlet, IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import { cog, flash, list } from 'ionicons/icons';
|
||||
|
||||
import Home from './Feed';
|
||||
import Lists from './Lists';
|
||||
import ListDetail from './ListDetail';
|
||||
import Settings from './Settings';
|
||||
|
||||
const Tabs = () => {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route path="/tabs/feed" component={Home} exact={true} />
|
||||
<Route path="/tabs/lists" component={Lists} exact={true} />
|
||||
<Route path="/tabs/lists/:listId" component={ListDetail} exact={true} />
|
||||
<Route path="/tabs/settings" component={Settings} exact={true} />
|
||||
<Route path="/tabs" render={() => <Redirect to="/tabs/feed" />} exact={true} />
|
||||
</IonRouterOutlet>
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1" href="/tabs/feed">
|
||||
<IonIcon icon={flash} />
|
||||
<IonLabel>Feed</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2" href="/tabs/lists">
|
||||
<IonIcon icon={list} />
|
||||
<IonLabel>Lists</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab3" href="/tabs/settings">
|
||||
<IonIcon icon={cog} />
|
||||
<IonLabel>Settings</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
@@ -1,32 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const { DarkMode } = Plugins;
|
||||
|
||||
const App = ({ children, className, ...props }) => {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
try {
|
||||
let darkmodeConfig = await DarkMode.isDarkModeOn();
|
||||
setDarkMode(darkmodeConfig.isDarkModeOn);
|
||||
DarkMode.addListener('darkModeStateChanged', state => {
|
||||
setDarkMode(state.isDarkModeOn);
|
||||
});
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames('flex h-screen flex-col', className, {
|
||||
dark: darkMode,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,17 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const Backdrop = ({ open, onClose }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
className={classNames('fixed z-10 inset-0 bg-black transition-opacity w-full h-full', {
|
||||
'pointer-events-none': !open,
|
||||
'opacity-10': open,
|
||||
'opacity-0': !open,
|
||||
})}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backdrop;
|
||||
@@ -1,15 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Button = ({ children, className, ...props }) => (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames(
|
||||
'inline-block text-xs font-medium leading-6 text-center uppercase transition rounded-lg ripple focus:outline-none',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
@@ -1,15 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Content = ({ className, visible, children, ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames(`h-full w-full overflow-auto py-2 absolute top-0`, className, {
|
||||
visible,
|
||||
invisible: !visible,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Content;
|
||||
@@ -1,9 +0,0 @@
|
||||
const Dialog = () => (
|
||||
<div className="fixed inset-0 w-full h-full flex align-center justify-center">
|
||||
<div className="w-200 bg-white rounded-xl">
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Dialog;
|
||||
@@ -1,3 +0,0 @@
|
||||
const EdgeDrag = () => null;
|
||||
|
||||
export default EdgeDrag;
|
||||
@@ -1,19 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Icon = ({ icon, ...props }) => {
|
||||
const svg = icon.replace('data:image/svg+xml;utf8,', '');
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames('ui-icon', {
|
||||
'ion-icon': true,
|
||||
'ion-color': true,
|
||||
[props.className]: true,
|
||||
})}
|
||||
>
|
||||
<div className="icon-inner" dangerouslySetInnerHTML={{ __html: svg }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
@@ -1,3 +0,0 @@
|
||||
const List = ({ children, ...props }) => <div {...props}>{children}</div>;
|
||||
|
||||
export default List;
|
||||
@@ -1,9 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const ListItem = ({ children, className, ...props }) => (
|
||||
<div className={classNames('p-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ListItem;
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useDrag } from 'react-use-gesture';
|
||||
|
||||
const { DarkMode } = Plugins;
|
||||
|
||||
const Menu = ({ open, onClose, children, className, ...props }) => {
|
||||
const ref = useRef();
|
||||
const [x, setX] = useState(-100000);
|
||||
const [rect, setRect] = useState(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
try {
|
||||
let darkmodeConfig = await DarkMode.isDarkModeOn();
|
||||
console.log({ open, darkMode: darkmodeConfig.isDarkModeOn });
|
||||
Plugins.StatusBar.setStyle({
|
||||
style: open && !darkmodeConfig.isDarkModeOn ? 'LIGHT' : 'DARK',
|
||||
}).catch(() => {});
|
||||
} catch (e) {}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
setRect(rect);
|
||||
setX(-rect.width);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setX(0);
|
||||
} else if (rect) {
|
||||
setX(-rect.width);
|
||||
}
|
||||
}, [rect, open]);
|
||||
|
||||
const bind = useDrag(
|
||||
({ down, movement: [mx] }) => {
|
||||
setX(mx > 0 ? 0 : mx);
|
||||
|
||||
if (down) {
|
||||
setDragging(true);
|
||||
} else {
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
// If the drag ended, snap the menu back
|
||||
if (!down) {
|
||||
const mid = -rect.width;
|
||||
if (x < mid / 2) {
|
||||
// Close
|
||||
setX(-rect.width);
|
||||
onClose();
|
||||
} else {
|
||||
// Re-open
|
||||
setX(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
axis: 'x',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
{...bind()}
|
||||
ref={ref}
|
||||
style={{
|
||||
paddingTop: `calc(env(safe-area-inset-top, 0px) + 8px)`,
|
||||
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + 8px)`,
|
||||
touchAction: 'pan-x',
|
||||
transform: `translateX(${x}px)`,
|
||||
}}
|
||||
className={classNames(
|
||||
'fixed z-40 transform-gpu translate w-48 h-full bg-gray-100 dark:bg-gray-800',
|
||||
className,
|
||||
{
|
||||
'transition-transform': !dragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -1,94 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useDrag } from 'react-use-gesture';
|
||||
import Store from '../../store';
|
||||
import { SafeAreaContext } from './SafeArea';
|
||||
|
||||
// A Modal window that slides in from offscreen and can be closed
|
||||
// by dragging.
|
||||
const Modal = ({ open, onClose, children }) => {
|
||||
const ref = useRef();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [rect, setRect] = useState(null);
|
||||
const [y, setY] = useState(100000);
|
||||
|
||||
const { top: safeAreaTop } = useContext(SafeAreaContext);
|
||||
|
||||
const _open = useCallback(() => {
|
||||
setY(safeAreaTop);
|
||||
}, [safeAreaTop]);
|
||||
|
||||
const _close = useCallback(() => {
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
setY(rect.height + safeAreaTop);
|
||||
}, [safeAreaTop, rect]);
|
||||
|
||||
// Get the layout rectangle for the modal
|
||||
useEffect(() => {
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
setRect(rect);
|
||||
_close();
|
||||
}, [safeAreaTop]);
|
||||
|
||||
// If open changes, open/close the modal
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
_open();
|
||||
} else {
|
||||
_close();
|
||||
}
|
||||
}, [rect, open, _open, _close]);
|
||||
|
||||
const bind = useDrag(
|
||||
({ down, movement: [mx, my] }) => {
|
||||
setY(my < 0 ? safeAreaTop : my + safeAreaTop);
|
||||
|
||||
if (down) {
|
||||
setDragging(true);
|
||||
} else {
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
// If the drag ended, snap the menu back open or close it
|
||||
if (!down) {
|
||||
const mid = rect.height;
|
||||
if (y > mid / 2) {
|
||||
// Close
|
||||
_close();
|
||||
onClose();
|
||||
} else {
|
||||
// Re-open
|
||||
_open();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
axis: 'y',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...bind()}
|
||||
className={classNames(
|
||||
'fixed z-40 top-5 transform transform-gpu w-full h-full bg-white rounded-t-xl',
|
||||
{
|
||||
'ease-in-out duration-300 transition-transformation': !dragging,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
'--safe-area-top': `env(safe-area-inset-top, 0px)`,
|
||||
height: `calc(100% - env(safe-area-inset-top, 0px) - 1.25rem)`,
|
||||
touchAction: 'pan-y',
|
||||
transform: `translateY(${y}px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -1,203 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import * as actions from '../../store/actions';
|
||||
|
||||
const Nav = ({ page }) => {
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
|
||||
const title = typeof page.title === 'function' ? page.title() : page.title;
|
||||
|
||||
useEffect(() => {
|
||||
Plugins.StatusBar.setStyle({
|
||||
style: 'DARK',
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="bg-gray-800 w-full flex-0 flex items-end flex-row z-10"
|
||||
style={{
|
||||
height: `calc(env(safe-area-inset-bottom, 0px) + 64px)`,
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 flex-1">
|
||||
<div className="relative flex items-center justify-between h-16">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 flex items-center sm:hidden"
|
||||
onClick={() => actions.setMenuOpen(true)}
|
||||
>
|
||||
{/* Mobile menu button*/}
|
||||
<button
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{/* Icon when menu is closed. */}
|
||||
{/*
|
||||
Heroicon name: menu
|
||||
|
||||
Menu open: "hidden", Menu closed: "block"
|
||||
*/}
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{/* Icon when menu is open. */}
|
||||
{/*
|
||||
Heroicon name: x
|
||||
|
||||
Menu open: "block", Menu closed: "hidden"
|
||||
*/}
|
||||
<svg
|
||||
className="hidden h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<h1 className="text-gray-50">{title}</h1>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:ml-6">
|
||||
<div className="flex space-x-4">
|
||||
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
|
||||
<a
|
||||
href="#"
|
||||
className="bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Team
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Projects
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Calendar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
<button
|
||||
className="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
onClick={() => actions.setNotificationsOpen(true)}
|
||||
>
|
||||
<span className="sr-only">View notifications</span>
|
||||
{/* Heroicon name: bell */}
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="ml-3 relative">
|
||||
<div>
|
||||
<button
|
||||
className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
id="user-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||
>
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/*
|
||||
Profile dropdown panel, show/hide based on dropdown state.
|
||||
|
||||
Entering: "transition ease-out duration-100"
|
||||
From: "transform opacity-0 scale-95"
|
||||
To: "transform opacity-100 scale-100"
|
||||
Leaving: "transition ease-in duration-75"
|
||||
From: "transform opacity-100 scale-100"
|
||||
To: "transform opacity-0 scale-95"
|
||||
*/}
|
||||
<div
|
||||
className={`${
|
||||
showProfileMenu ? '' : 'hidden'
|
||||
} origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="user-menu"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
||||
role="menuitem"
|
||||
>
|
||||
Your Profile
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
||||
role="menuitem"
|
||||
>
|
||||
Settings
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
||||
role="menuitem"
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
@@ -1,37 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Transition } from 'react-transition-group';
|
||||
|
||||
const duration = 500;
|
||||
|
||||
const defaultStyle = {
|
||||
transition: `opacity ${duration}ms ease-in-out`,
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const transitionStyles = {
|
||||
entering: { opacity: 1 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
exited: { opacity: 0 },
|
||||
};
|
||||
|
||||
const PageStack = ({ children, className, ...props }) => (
|
||||
<div {...props} className={classNames('flex-1 z-0 overflow-hidden relative', className)}>
|
||||
<Transition in={true} duration={duration}>
|
||||
{state => (
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
...defaultStyle,
|
||||
...transitionStyles[state],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PageStack;
|
||||
@@ -1,5 +0,0 @@
|
||||
# UI Components
|
||||
|
||||
These components are a mini-tailwind UI framework for building native mobile apps with web technologies.
|
||||
|
||||
These components are meant to be modified and customized to fit your app, and provide many common mobile UI patterns.
|
||||
@@ -1,38 +0,0 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
|
||||
export const SafeAreaContext = createContext({ top: 0, bottom: 0 });
|
||||
|
||||
// This provider reads and stores the computed safe area
|
||||
// on devices with notches/etc. (iPhone X, for example)
|
||||
//
|
||||
// This is done by reading the CSS Properties --safe-area-top and --safe-area-bottom
|
||||
// and then storing them as state values.
|
||||
//
|
||||
// These values are useful for JS-driven interactions, such as a modal that
|
||||
// will drag in and out but needs to offset for the safe region.
|
||||
export const SafeAreaProvider = ({ children }) => {
|
||||
const [safeAreaTop, setSafeAreaTop] = useState(0);
|
||||
const [safeAreaBottom, setSafeAreaBottom] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// I don't know why, but we can't get the value of this CSS variable
|
||||
// until a bit of a delay, maybe something with Next?
|
||||
setTimeout(() => {
|
||||
const safeAreaTop = parseInt(
|
||||
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-top')
|
||||
);
|
||||
const safeAreaBottom = window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--safe-area-bottom');
|
||||
|
||||
setSafeAreaTop(safeAreaTop);
|
||||
setSafeAreaBottom(safeAreaBottom);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaContext.Provider value={{ top: safeAreaTop, bottom: safeAreaBottom }}>
|
||||
{children}
|
||||
</SafeAreaContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
|
||||
const Tab = ({ title, icon, selected, selectedIcon, onClick }) => (
|
||||
<a
|
||||
onClick={onClick}
|
||||
href="#"
|
||||
className={classNames('px-6 rounded-md text-sm text-center font-medium cursor-pointer', {
|
||||
'text-gray-500 dark:text-white': !selected,
|
||||
'text-gray-800 dark:text-gray-600': selected,
|
||||
})}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
icon={selected ? selectedIcon : icon}
|
||||
className="cursor-pointer"
|
||||
style={{ fontSize: '18px' }}
|
||||
/>
|
||||
)}
|
||||
<label className="block cursor-pointer">{title}</label>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default Tab;
|
||||
@@ -1,13 +0,0 @@
|
||||
const TabBar = ({ children }) => (
|
||||
<nav
|
||||
id="tab-bar"
|
||||
className="py-2 h-16 w-full flex justify-center items-start bg-gray-50 z-10 dark:bg-gray-800"
|
||||
style={{
|
||||
height: `calc(env(safe-area-inset-bottom, 0px) + 56px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
|
||||
export default TabBar;
|
||||
@@ -1,5 +0,0 @@
|
||||
import ReactToggle from 'react-toggle';
|
||||
|
||||
const Toggle = props => <ReactToggle {...props} icons={false} />;
|
||||
|
||||
export default Toggle;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
const VirtualScroll = props => <Virtuoso {...props} />;
|
||||
|
||||
export default VirtualScroll;
|
||||
@@ -0,0 +1,62 @@
|
||||
export const images = [
|
||||
'https://images.unsplash.com/photo-1610235554447-41505d7962f8?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=882&q=80',
|
||||
'https://images.unsplash.com/photo-1610212594948-370947a3ba0b?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=934&q=80',
|
||||
'https://images.unsplash.com/photo-1610155180433-9994da6a323b?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
||||
];
|
||||
|
||||
export const homeItems = [
|
||||
{
|
||||
title: 'Exploring Maui',
|
||||
type: 'Blog',
|
||||
text: 'We just got back from a trip to Maui, and we had a great time...',
|
||||
author: 'Max Lynch',
|
||||
authorAvatar: '/img/max.jpg',
|
||||
image: images[0],
|
||||
},
|
||||
{
|
||||
title: 'Arctic Adventures',
|
||||
type: 'Blog',
|
||||
text:
|
||||
'Last month we took a trek to the Arctic Circle. The isolation was just what we needed after...',
|
||||
author: 'Max Lynch',
|
||||
authorAvatar: '/img/max.jpg',
|
||||
image: images[1],
|
||||
},
|
||||
{
|
||||
title: 'Frolicking in the Faroe Islands',
|
||||
type: 'Blog',
|
||||
text:
|
||||
'The Faroe Islands are a North Atlantic archipelago located 320 kilometres (200 mi) north-northwest of Scotland...',
|
||||
author: 'Max Lynch',
|
||||
authorAvatar: '/img/max.jpg',
|
||||
image: images[2],
|
||||
},
|
||||
];
|
||||
|
||||
export const notifications = [
|
||||
{ title: 'New friend request', when: '6 hr' },
|
||||
{ title: 'Please change your password', when: '1 day' },
|
||||
{ title: 'You have a new message', when: '2 weeks' },
|
||||
{ title: 'Welcome to the app!', when: '1 month' },
|
||||
];
|
||||
|
||||
// Some fake lists
|
||||
export const lists = [
|
||||
{
|
||||
name: 'Groceries',
|
||||
id: 'groceries',
|
||||
items: [{ name: 'Apples' }, { name: 'Bananas' }, { name: 'Milk' }, { name: 'Ice Cream' }],
|
||||
},
|
||||
{
|
||||
name: 'Hardware Store',
|
||||
id: 'hardware',
|
||||
items: [
|
||||
{ name: 'Circular Saw' },
|
||||
{ name: 'Tack Cloth' },
|
||||
{ name: 'Drywall' },
|
||||
{ name: 'Router' },
|
||||
],
|
||||
},
|
||||
{ name: 'Work', id: 'work', items: [{ name: 'TPS Report' }, { name: 'Set up email' }] },
|
||||
{ name: 'Reminders', id: 'reminders' },
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
basePath: ''
|
||||
};
|
||||
@@ -9,9 +9,12 @@
|
||||
"export": "next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^2.4.6",
|
||||
"@capacitor/cli": "^2.4.5",
|
||||
"@capacitor/core": "^2.4.5",
|
||||
"@capacitor/ios": "^2.4.5",
|
||||
"@ionic/react": "^5.5.2",
|
||||
"@ionic/react-router": "^5.5.2",
|
||||
"autoprefixer": "^10.1.0",
|
||||
"capacitor-dark-mode": "^1.0.5",
|
||||
"classnames": "^2.2.6",
|
||||
@@ -19,6 +22,7 @@
|
||||
"postcss": "^8.2.1",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-virtuoso": "^1.1.1",
|
||||
"tailwindcss": "^2.0.2"
|
||||
},
|
||||
@@ -26,9 +30,6 @@
|
||||
"ionicons": "^5.2.3",
|
||||
"prettier": "^2.2.1",
|
||||
"pullstate": "^1.20.5",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-toggle": "^4.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-use-gesture": "^9.0.0-beta.11",
|
||||
"reselect": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const App = dynamic(() => import('../components/AppShell'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Index() {
|
||||
return <App />;
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import Head from 'next/head';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import Store from '../store';
|
||||
import '@ionic/react/css/core.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';
|
||||
|
||||
import '../styles/global.css';
|
||||
import '../styles/variables.css';
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
|
||||