Merge pull request #3 from mlynch/main

Merge upstream
This commit is contained in:
Leo Giovanetti
2021-01-30 18:31:03 -03:00
committed by GitHub
176 changed files with 2254 additions and 1436 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
@@ -0,0 +1,2 @@
#Wed Jan 13 17:35:11 CST 2021
gradle.version=5.6.4
+48 -23
View File
@@ -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!*
![Screenshot](./screenshot.png)
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.
![Screenshot](./ss.png)
## 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
+91
View File
@@ -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
Generated Vendored
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/workspace.xml
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+51
View File
@@ -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")
}
+19
View File
@@ -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()
}
+21
View File
@@ -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());
}
}
+63
View File
@@ -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);
}});
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

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>
Binary file not shown.

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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

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>
+6
View File
@@ -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);
}
}
+29
View File
@@ -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
}
+6
View File
@@ -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')
+24
View File
@@ -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
Binary file not shown.
+5
View File
@@ -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
Vendored Executable
+188
View File
@@ -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" "$@"
+100
View File
@@ -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
+5
View File
@@ -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'
+17
View File
@@ -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'
}
+25
View File
@@ -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;
+87
View File
@@ -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;
-44
View File
@@ -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;
-45
View File
@@ -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;
+71
View File
@@ -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;
-32
View File
@@ -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;
+48 -45
View File
@@ -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>
);
};
+33 -28
View File
@@ -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>
);
};
+55
View File
@@ -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;
+33 -20
View File
@@ -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>
);
};
+39
View File
@@ -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;
-32
View File
@@ -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;
-17
View File
@@ -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;
-15
View File
@@ -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;
-15
View File
@@ -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;
-9
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
const EdgeDrag = () => null;
export default EdgeDrag;
-19
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
const List = ({ children, ...props }) => <div {...props}>{children}</div>;
export default List;
-9
View File
@@ -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;
-91
View File
@@ -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;
-94
View File
@@ -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;
-203
View File
@@ -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;
-37
View File
@@ -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;
-5
View File
@@ -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.
-38
View File
@@ -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>
);
};
-24
View File
@@ -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;
-13
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import ReactToggle from 'react-toggle';
const Toggle = props => <ReactToggle {...props} icons={false} />;
export default Toggle;
-5
View File
@@ -1,5 +0,0 @@
import { Virtuoso } from 'react-virtuoso';
const VirtualScroll = props => <Virtuoso {...props} />;
export default VirtualScroll;
+62
View File
@@ -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' },
];
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
basePath: ''
};
+506 -336
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -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"
}
+9
View File
@@ -0,0 +1,9 @@
import dynamic from 'next/dynamic';
const App = dynamic(() => import('../components/AppShell'), {
ssr: false,
});
export default function Index() {
return <App />;
}
+8 -2
View File
@@ -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 (

Some files were not shown because too many files have changed in this diff Show More