organize
This commit is contained in:
@@ -1 +0,0 @@
|
||||
{:lint-as {reagent.core/with-let clojure.core/let}}
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [PEZ, bpringe]
|
||||
# patreon: # Replace with a single Patreon username
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: # Replace with a single Liberapay username
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,22 +0,0 @@
|
||||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
out/
|
||||
.shadow-cljs/
|
||||
.nrepl-port
|
||||
pom.xml
|
||||
.idea/
|
||||
*.iml
|
||||
app
|
||||
.cpcache
|
||||
/web-build
|
||||
.calva/output-window/
|
||||
.DS_Store
|
||||
|
||||
.lsp/.cache
|
||||
.clj-kondo/.cache
|
||||
.lsp/sqlite.db
|
||||
@@ -1,13 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="REPL" type="ClojureREPL" factoryName="Remote" activateToolWindowBeforeRun="false">
|
||||
<module name="rn-rf-shadow" />
|
||||
<setting name="host" value="" />
|
||||
<setting name="port" value="0" />
|
||||
<setting name="replType" value="NREPL" />
|
||||
<setting name="configType" value="PORT_FILE" />
|
||||
<setting name="replPortFileType" value="STANDARD" />
|
||||
<setting name="customPortFile" value="" />
|
||||
<setting name="fixLineNumbers" value="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,209 +0,0 @@
|
||||
# React Native using shadow-cljs in 3 minutes
|
||||
|
||||
The fastest way a [ClojureScript](https://clojurescript.org/) coder can get started with React Native development. *Prove me wrong.*
|
||||
|
||||
This is an example project, only slightly beyond *Hello World*, using: [shadow-cljs](https://github.com/thheller/shadow-cljs), [React Native](https://facebook.github.io/react-native/), [Expo](https://expo.io/), [Reagent](https://reagent-project.github.io/), and [re-frame](https://github.com/Day8/re-frame).
|
||||
|
||||
<div style="display: flex; justify-content: space-around;">
|
||||
<div style="flex: 1"><img src="./rn-rf-shadow.png" width="240" /></div>
|
||||
<div style="flex: 1">Check this video out for a demo of this project.<br>
|
||||
<a href="https://www.youtube.com/watch?v=QsUj7HO5xDg"><img src="https://img.youtube.com/vi/QsUj7HO5xDg/maxresdefault.jpg" width="320px"><br>
|
||||
ClojureScript ❤️ React Native</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Follow along to get started. There are instructions for [Calva](http://calva.io), [Emacs/CIDER](https://cider.mx), [Cursive](https://cursive-ide.com), and the command line. It is assumed you have Java and Node installad as well as dev tool chains for the platforms you are targeting. (If you are targeting the Web, then Chrome is enough.)
|
||||
|
||||
## Installing
|
||||
|
||||
To facilitate that you can easily try this out without installing anything globally on your machine, this project installs everything it needs locally in `node_modules`. Then `npx` is used to execute tools like `expo-cli`.
|
||||
|
||||
To install dependencies, and setup the project, run:
|
||||
|
||||
1. `npm i`
|
||||
|
||||
From there use your favorite editor and/or the prompt.
|
||||
|
||||
## Using VS Code + Calva
|
||||
|
||||
Assuming you have installed the [Calva](https://calva.io) extension in VS Code:
|
||||
|
||||
### Build and start the app, and connect Calva
|
||||
|
||||
1. Open the project in VS Code. Then:
|
||||
1. Run the Calva command **Start a Project REPL and Connect (aka Jack-in)**
|
||||
1. Wait for shadow to build the project.
|
||||
|
||||
### Start Expo
|
||||
|
||||
1. Then **Run Build Task**. This will start Expo and the Metro
|
||||
bundler. Wait for expo to show its menu options in the terminal pane.
|
||||
1. In the expo menu press w for **open web**.
|
||||
|
||||
The app now should be running in your web browser and Calva automatically connects to it. Confirm this by evaluating something like this in Calva (from a cljs file or in the REPL window):
|
||||
|
||||
``` clojure
|
||||
(js/alert "Hello world!")
|
||||
```
|
||||
|
||||
You should see the alert pop up where the app is running.
|
||||
|
||||
Of course you should try to fire up the app on all simulators, emulators and phones you have as well. Please note that Calva will only be connected to one of your apps at a time, and it is a bit arbitrary which one. Use `(js/alert)` to check this.
|
||||
|
||||
## Using Emacs with CIDER
|
||||
|
||||
Open Emacs and a bash shell:
|
||||
|
||||
1. Run `npx shadow-cljs compile :app` to perform an initial build of the app.
|
||||
1. In Emacs open one of the files in the project (`deps.edn` is fine)
|
||||
1. From that buffer, do `cider-jack-in-clojurescript` [C-c M-J] to
|
||||
launch a REPL. Follow the series of interactive prompts in the
|
||||
minibuffer:
|
||||
1. select `shadow-cljs` as the command to launch
|
||||
1. select `shadow` as the repl type
|
||||
1. select `:app` as the build to connect
|
||||
1. and optionally answer `y` or `n` to the final question about
|
||||
opening the `shadow-cljs` UI in a browser.
|
||||
At this point `shadow-cljs` will be watching the project folder and
|
||||
running new builds of the app if any files are changed. You'll also
|
||||
have a REPL prompt, *however the REPL doesn't work because it isn't
|
||||
connected to anything. The app isn't running yet.*
|
||||
1. In a shell run `npm run ios` (same as `npx expo start -i`). This starts
|
||||
the Metro bundler, perform the bundling, launch the iPhone
|
||||
simulator, and transmit the bundled app. Be patient at this step as
|
||||
it can take many seconds to complete. When the app is finally
|
||||
running expo will display the message:
|
||||
|
||||
WebSocket connected!
|
||||
REPL init successful
|
||||
1. Once you see that the REPL is initalized, you can return to Emacs
|
||||
and confirm the REPL is connected and functional:
|
||||
``` clojure
|
||||
cljs.user> (js/alert "hello world!")
|
||||
```
|
||||
Which should pop-up a modal alert in the simulator, confirming the
|
||||
app is running and the REPL is connected end to end.
|
||||
|
||||
## Using IntelliJ + Cursive REPL
|
||||
|
||||
1. Follow the instructions specified in [Or the Command line](#or-the-command-line).
|
||||
2. Create a Maven POM using `shadow-cljs pom`, as described in the [Shadow doc](https://shadow-cljs.github.io/docs/UsersGuide.html#_cursive).
|
||||
3. There are now two options
|
||||
1. If you already have a project open, open the project in IntelliJ using _File | New | Project from existing sources..._ and indicating the `pom.xml` file.
|
||||
2. If you're at the welcome screen, press the "Open" button and navigate to the `pom.xml`.
|
||||
5. Ensure the project has an SDK configured using _File | Project Structure_, and checking under `Project`.
|
||||
7. The project comes with a REPL run configuration called "REPL". Run the REPL using the _Run | Run 'REPL'_ menu item, or the toolbar button.
|
||||
8. Run the commands in [Using ClojureScript REPL](#using-clojurescript-repl)
|
||||
|
||||
## Or the Command line
|
||||
```sh
|
||||
$ npm i
|
||||
$ npx shadow-cljs watch app
|
||||
# wait for first compile to finish or expo gets confused
|
||||
# on another terminal tab/window:
|
||||
$ npm start
|
||||
```
|
||||
This will run Expo DevTools at http://localhost:19002/
|
||||
|
||||
To run the app in browser using expo-web (react-native-web), press `w` in the same terminal after expo devtools is started.
|
||||
This should open the app automatically on your browser after the web version is built. If it does not open automatically, open http://localhost:19006/ manually on your browser.
|
||||
|
||||
Note that you can also run the following instead of `npm start` to run the app in browser:
|
||||
```
|
||||
# same as npx expo start --web
|
||||
$ npm run web
|
||||
|
||||
# or
|
||||
|
||||
# same as npx expo start --web-only
|
||||
$ npm run web-only
|
||||
```
|
||||
|
||||
### Using ClojureScript REPL
|
||||
Once the app is deployed and opened in phone/simulator/emulator/browser, connect to nrepl and run the following:
|
||||
|
||||
```clojure
|
||||
(shadow/nrepl-select :app)
|
||||
```
|
||||
|
||||
NB: _Calva users don't need to do ^ this ^._
|
||||
|
||||
To test the REPL connection:
|
||||
|
||||
```clojure
|
||||
(js/alert "Hello from Repl")
|
||||
```
|
||||
|
||||
### Command line CLJS REPL
|
||||
|
||||
Shadow can start a CLJS repl for you, if you prefer to stay at the terminal prompt:
|
||||
|
||||
```bash
|
||||
$ npx shadow-cljs cljs-repl :app
|
||||
```
|
||||
|
||||
## Disabling Expo Fast Refresh
|
||||
|
||||
You will need to disable **Fast Refresh** provided by the Expo client, which conflicts with shadow-cljs hot reloading. You really want to use Shadow's, because it is way better and way faster than the Expo stuff is.
|
||||
|
||||
For the iOS and Android there is a **Disable Fast Refresh** option in the [development menu](https://docs.expo.io/workflow/debugging/#developer-menu). NB: _Often you need to first enable it and then disable it._
|
||||
|
||||
For web there may be some way to disable it via a `webpack.config` file as per [this example](https://docs.expo.dev/guides/customizing-webpack/#example). But failing that, once the app has loaded you can block requests to/from `localhost:19006/*` (the Webpack dev server) in devtools [like so](https://github.com/facebook/create-react-app/issues/2519#issuecomment-318867289), for instance by right-clicking on a request in the Network tab, selecting `Block request URL`, then editing the pattern. In Chrome this looks something like:
|
||||
|
||||

|
||||

|
||||
|
||||
This workaround is far from ideal, because the block needs to be manually toggled *off* whenever a full refresh is required (e.g. to load a new file), then back on again. But it seems to do the job.
|
||||
|
||||
## Production builds
|
||||
|
||||
A production build involves first asking shadow-cljs to build a release, then to ask Expo to work in Production Mode.
|
||||
|
||||
1. Kill the watch and expo tasks.
|
||||
1. Execute `shadow-cljs release app`
|
||||
1. Start the expo task (as per above)
|
||||
1. Enable Production mode.
|
||||
1. Start the app.
|
||||
|
||||
### Using EAS Build
|
||||
|
||||
`expo build` is the classic way of building an Expo app, and `eas build` is the new version of `expo build`. Using EAS Build currently requires an Expo account with a paid plan subscription.
|
||||
|
||||
The steps below provide an example of using EAS Build to build an apk file to run on an Android emulator or device.
|
||||
|
||||
0. Install the latest EAS CLI by running `npm install -g eas-cli`
|
||||
0. Log into your Expo account
|
||||
0. Configure EAS Build in your project with `eas build:configure`.
|
||||
0. Make your eas.json file contents look like this:
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"production": {},
|
||||
"development": {
|
||||
"distribution": "internal",
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
},
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
0. Commit your changes, run `eas build --profile development`, and follow the prompts.
|
||||
0. Navigate to the URL given by the command to monitor the build. When it completes, download the apk and install it on your device or emulator.
|
||||
|
||||
See [the EAS Build docs](https://docs.expo.dev/build/introduction/) for more information.
|
||||
|
||||
If you want to use EAS Build with a project not based on this template, see [this PR](https://github.com/PEZ/rn-rf-shadow/pull/24) for information about how your project can be set up to avoid an error during the build process.
|
||||
|
||||
Note: The `eas-build-pre-install.sh` script makes EAS install Java in the MacOS environment when running a build for iOS. This ensures that shadow-cljs can be run in the EAS pipeline to build your ClojureScript code.
|
||||
|
||||
## React Navigation included
|
||||
|
||||
The app is setup to use [React Navigation](https://reactnavigation.org/). If you don't need that in your app, just remove it.
|
||||
|
||||
## Happy Hacking! ❤️
|
||||
|
||||
Please don't hesitate to star the project repository.
|
||||
@@ -0,0 +1,16 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
@@ -0,0 +1,24 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "expo-root-project"
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# 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
|
||||
|
||||
# Enable AAPT2 PNG crunching
|
||||
android.enablePngCrunchInReleaseBuilds=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=true
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
|
||||
# Enable network inspector
|
||||
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# 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
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
@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
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@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=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@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% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,39 @@
|
||||
pluginManagement {
|
||||
def reactNativeGradlePlugin = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||
}.standardOutput.asText.get().trim()
|
||||
).getParentFile().absolutePath
|
||||
includeBuild(reactNativeGradlePlugin)
|
||||
|
||||
def expoPluginsPath = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||
}.standardOutput.asText.get().trim(),
|
||||
"../android/expo-gradle-plugin"
|
||||
).absolutePath
|
||||
includeBuild(expoPluginsPath)
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.facebook.react.settings")
|
||||
id("expo-autolinking-settings")
|
||||
}
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||
}
|
||||
}
|
||||
expoAutolinking.useExpoModules()
|
||||
|
||||
rootProject.name = 'Todo App'
|
||||
|
||||
expoAutolinking.useExpoVersionCatalog()
|
||||
|
||||
include ':app'
|
||||
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||
+4
-25
@@ -1,35 +1,14 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Todo App",
|
||||
"slug": "todo-expo",
|
||||
"slug": "todo-expo-js",
|
||||
"privacy": "public",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android",
|
||||
"web"
|
||||
],
|
||||
"platforms": ["ios", "android"],
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"plugins": [
|
||||
"expo-sqlite"
|
||||
],
|
||||
"plugins": ["expo-sqlite"],
|
||||
"android": {
|
||||
"package": "com.anonymous.todoexpo"
|
||||
"package": "com.anonymous.todoexpojs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB |
Executable → Regular
@@ -1,28 +0,0 @@
|
||||
{:paths ["src/main" "src/test"]
|
||||
|
||||
:deps {org.clojure/clojure {:mvn/version "1.12.1"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.60"}
|
||||
|
||||
;; React and Re-frame
|
||||
reagent/reagent {:mvn/version "1.2.0"}
|
||||
re-frame/re-frame {:mvn/version "1.3.0"}
|
||||
day8.re-frame/http-fx {:mvn/version "0.2.4"}
|
||||
day8.re-frame/tracing {:mvn/version "0.6.2"}}
|
||||
|
||||
:aliases
|
||||
{:dev {:extra-deps {thheller/shadow-cljs {:mvn/version "2.28.10"}
|
||||
nrepl/nrepl {:mvn/version "1.1.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.45.0"}}}
|
||||
|
||||
;; Shadow-cljs build
|
||||
:shadow {:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
|
||||
;; For connecting via MCP from pez-client side
|
||||
:mcp-client {:extra-deps {org.slf4j/slf4j-nop {:mvn/version "2.0.16"}
|
||||
com.bhauman/clojure-mcp {:git/url "https://github.com/bhauman/clojure-mcp.git"
|
||||
:git/tag "v0.1.8-alpha"
|
||||
:git/sha "457f197"}}
|
||||
:exec-fn clojure-mcp.main/start-mcp-server
|
||||
:exec-args {:port 7892 ; Different port for pez-client REPL
|
||||
:type :shadow-cljs
|
||||
:shadow-build "app"}}}}
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If the OS is MacOS, install Java so that the shadow-cljs build succeeds
|
||||
if [[ $OSTYPE == "darwin"* ]]; then
|
||||
brew install openjdk@11
|
||||
sudo ln -sfn /opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-11.jdk || { echo 'Homebrew JDK 11 not found?' ; exit 1; }
|
||||
echo 'export PATH="/opt/homebrew/opt/openjdk@11/bin:$PATH"' >>~/.zshrc
|
||||
export CPPFLAGS="-I/opt/homebrew/opt/openjdk@11/include"
|
||||
fi
|
||||
@@ -1,12 +0,0 @@
|
||||
--- worker.js 2018-11-21 14:46:01.271844624 -0700
|
||||
+++ worker.js 2018-11-21 14:45:52.517615272 -0700
|
||||
@@ -218,7 +218,7 @@
|
||||
}
|
||||
|
||||
if (!options.transformOptions.dev) {
|
||||
- plugins.push([constantFoldingPlugin, opts]);
|
||||
+ // plugins.push([constantFoldingPlugin, opts]);
|
||||
plugins.push([inlinePlugin, opts]);
|
||||
}var _transformFromAstSync = transformFromAstSync(ast, "", {
|
||||
ast: true,
|
||||
babelrc: false,
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/Usr/bin/env joker
|
||||
|
||||
;; Usage: toolchain-report
|
||||
;;
|
||||
;; This tool queries a set of defined packages in your "toolchain" and
|
||||
;; reports the versions of those packages in a map. There are no
|
||||
;; command line flags or options for this script.
|
||||
|
||||
(ns main
|
||||
(:require [joker.os :as os]))
|
||||
|
||||
|
||||
;; The "toolchain" is defined as a vector of maps that define how to
|
||||
;; query for version. We expect every tool has some command line flag
|
||||
;; that writes the version info to stdout or stderr, but each tool
|
||||
;; might format that output in some unique way. Therefore we specify a
|
||||
;; regular expression pattern that will be used to extract the version
|
||||
;; from the output.
|
||||
;;
|
||||
;; Easiest to show an example:
|
||||
;;
|
||||
;; {
|
||||
;; :key :yarn ; each tool has a unique key
|
||||
;; :cmd ["yarn" "-v"] ; executing this prints the version
|
||||
;; :pattern #"(.*)\n" ; this regexp extracts the version #
|
||||
;; :output :out ; :out = stdout, :err = stderr
|
||||
;; }
|
||||
;;
|
||||
;; The default is to grab everything from stdout up to the first
|
||||
;; newline '\n', so our example can be shortened:
|
||||
;;
|
||||
;; {:key :yarn :cmd ["yarn" "-v"]}
|
||||
;;
|
||||
|
||||
(def toolchain
|
||||
[
|
||||
;; operating system
|
||||
{:key :os-name
|
||||
:cmd ["clojure" "-e" "(System/getProperty \"os.name\")"]
|
||||
:pattern #"\"(.*)\""}
|
||||
{:key :os-version
|
||||
:cmd ["clojure" "-e" "(System/getProperty \"os.version\")"]
|
||||
:pattern #"\"(.*)\""}
|
||||
{:key :todays-date
|
||||
:cmd ["clojure" "-e" "(.toString (java.time.LocalDate/now))"]}
|
||||
|
||||
;; clojure/clojurescript tools
|
||||
{:key :clojure
|
||||
:cmd ["clj" "-Stree"]
|
||||
:pattern #"org.clojure/clojure\s+(.*)\n"}
|
||||
{:key :clojurescript
|
||||
:cmd ["clj" "-Stree"]
|
||||
:pattern #"org.clojure/clojurescript\s+(.*)\n"}
|
||||
{:key :shadow-cljs-jar
|
||||
:cmd ["shadow-cljs" "info"]
|
||||
:pattern #"jar:\s+(.*)\n"}
|
||||
{:key :shadow-cljs-cli
|
||||
:cmd ["shadow-cljs" "info"]
|
||||
:pattern #"cli:\s+(.*)\n"}
|
||||
|
||||
;; react native toolchain
|
||||
{:key :expo-cli :cmd ["expo-cli" "-V"]}
|
||||
|
||||
;; javascript tools
|
||||
{:key :node :cmd ["node" "-v"]}
|
||||
{:key :yarn :cmd ["yarn" "-v"]}
|
||||
|
||||
;; libraries / packages
|
||||
{:key :expo-js
|
||||
:cmd ["yarn" "list"]
|
||||
:pattern #"\s+expo@(.*)\n"}
|
||||
{:key :react
|
||||
:cmd ["yarn" "list"]
|
||||
:pattern #"\s+react@(.*)\n"}
|
||||
{:key :react-native
|
||||
:cmd ["yarn" "list"]
|
||||
:pattern #"\s+react-native@(.*)\n"}
|
||||
{:key :reagent
|
||||
:cmd ["clj" "-Stree"]
|
||||
:pattern #"reagent/reagent\s+(.*)\n"}
|
||||
{:key :re-frame
|
||||
:cmd ["clj" "-Stree"]
|
||||
:pattern #"re-frame/re-frame\s+(.*)\n"}
|
||||
|
||||
;; java jdk
|
||||
{:key :jdk-vendor
|
||||
:cmd ["java" "-version"]
|
||||
:output :err
|
||||
:pattern #"(.*)\s+version"}
|
||||
{:key :jdk-version
|
||||
:cmd ["java" "-version"]
|
||||
:output :err
|
||||
:pattern #"version\s+\"(.*)\"\n"}
|
||||
])
|
||||
|
||||
(def sh-memoized (memoize #(apply os/sh %)))
|
||||
|
||||
(defn check [{:keys [key cmd output pattern]
|
||||
:or {output :out pattern #"(.*)\n"}}]
|
||||
(let [result (try (sh-memoized cmd)
|
||||
(catch Error e {:success nil}))]
|
||||
(if (:success result)
|
||||
{key (-> (re-find pattern (output result))
|
||||
(get 1))}
|
||||
{key nil})))
|
||||
|
||||
(defn print-sorted-keys [m]
|
||||
;; hack because joker doesn't provide sorted-map
|
||||
(println "{")
|
||||
(doseq [k (sort (keys m))]
|
||||
(printf " %s \"%s\"\n" k (k m)))
|
||||
(println "}"))
|
||||
|
||||
;; run all queries and print the result
|
||||
(print-sorted-keys (->> (map check toolchain)
|
||||
(apply merge)))
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# fulcro doesn't properly type hint these since they are usually covered
|
||||
# by react externs. we are not processing any JS however so those are missing
|
||||
render
|
||||
props
|
||||
state
|
||||
+3
-41
@@ -1,42 +1,4 @@
|
||||
// Disable all Expo/Metro hot reload mechanisms BEFORE loading app
|
||||
// This ensures only shadow-cljs hot reload remains active
|
||||
if (typeof window !== 'undefined') {
|
||||
// Disable React Fast Refresh
|
||||
window.$RefreshReg$ = () => {};
|
||||
window.$RefreshSig$ = () => type => type;
|
||||
|
||||
// Disable Metro hot update
|
||||
window.metroHotUpdateModule = () => {};
|
||||
window.__accept = () => {};
|
||||
|
||||
// Disable webpack hot update (fallback)
|
||||
window.webpackHotUpdate = () => {};
|
||||
|
||||
// Override WebSocket to block Metro connections
|
||||
// const OriginalWebSocket = window.WebSocket;
|
||||
// window.WebSocket = function(url) {
|
||||
// if (url && (url.includes('metro') ||
|
||||
// url.includes('packager') ||
|
||||
// url.includes('19001') ||
|
||||
// url.includes('19000') ||
|
||||
// url.includes('8081'))) {
|
||||
// console.log('Blocked Metro WebSocket connection:', url);
|
||||
// return {
|
||||
// send: () => {},
|
||||
// close: () => {},
|
||||
// addEventListener: () => {},
|
||||
// removeEventListener: () => {}
|
||||
// };
|
||||
// }
|
||||
// return new OriginalWebSocket(url);
|
||||
// };
|
||||
|
||||
// Prevent module.hot usage
|
||||
if (typeof module !== 'undefined' && module.hot) {
|
||||
delete module.hot;
|
||||
}
|
||||
|
||||
console.log('Expo/Metro hot reload disabled - shadow-cljs will handle hot reload');
|
||||
}
|
||||
import { registerRootComponent } from 'expo';
|
||||
import App from './src/App';
|
||||
|
||||
import './app/index.js';
|
||||
registerRootComponent(App);
|
||||
|
||||
Generated
+1086
-14345
File diff suppressed because it is too large
Load Diff
+8
-23
@@ -1,40 +1,25 @@
|
||||
{
|
||||
"name": "todo-expo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"server": "npx shadow-cljs server",
|
||||
"start": "npx expo start",
|
||||
"android": "npx expo start --android",
|
||||
"ios": "npx expo start --ios",
|
||||
"web": "npx expo start --web",
|
||||
"web-only": "npx expo start --web-only",
|
||||
"release": "npx shadow-cljs release app",
|
||||
"eject": "npx expo eject",
|
||||
"eas-build-pre-install": "bash eas-build-pre-install.sh",
|
||||
"eas-build-post-install": "npx shadow-cljs release app"
|
||||
"ios": "npx expo start --ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-navigation/native": "^6.1.6",
|
||||
"@react-navigation/native-stack": "^6.9.12",
|
||||
"babel-preset-expo": "^55.0.8",
|
||||
"create-react-class": "^15.7.0",
|
||||
"expo": "^55.0.2",
|
||||
"expo-cli": "^6.3.8",
|
||||
"expo-sqlite": "~55.0.10",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-native": "0.83.2",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"react-native-web": "^0.21.0"
|
||||
"react-native-screens": "~4.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-class-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.28.6",
|
||||
"shadow-cljs": "^2.23.3"
|
||||
},
|
||||
"private": true
|
||||
"babel-preset-expo": "^55.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
@@ -1,10 +0,0 @@
|
||||
{:source-paths ["src/main"]
|
||||
|
||||
:dependencies [[reagent "1.2.0"]
|
||||
[re-frame "1.3.0"]]
|
||||
|
||||
:builds {:app {:target :react-native
|
||||
:init-fn todo.app/init
|
||||
:output-dir "app"
|
||||
:compiler-options {:infer-externs :auto}
|
||||
:devtools {:autoload true}}}}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { LogBox, StatusBar } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
LogBox.ignoreAllLogs();
|
||||
import { TodoProvider, useTodoState } from './state';
|
||||
import * as db from './db';
|
||||
import MainScreen from './components/MainScreen';
|
||||
import TodoForm from './components/TodoForm';
|
||||
|
||||
function AppContent() {
|
||||
const { state, dispatch } = useTodoState();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await db.initDb();
|
||||
const todos = await db.getAllTodos();
|
||||
dispatch({ type: 'TODOS_LOADED', rows: todos });
|
||||
} catch (err) {
|
||||
console.error('SQLite init error:', err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#6200EE" />
|
||||
<MainScreen />
|
||||
{state.showForm && <TodoForm />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<TodoProvider>
|
||||
<AppContent />
|
||||
</TodoProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
import { categories } from '../state';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function CategoryPicker({ selected, onSelect }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{categories.map(cat => {
|
||||
const isSelected = cat === selected;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={cat}
|
||||
onPress={() => onSelect(cat)}
|
||||
style={[
|
||||
styles.pill,
|
||||
{
|
||||
backgroundColor: isSelected ? colors.primary : colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginVertical: 8,
|
||||
},
|
||||
pill: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
import { useTodoState, getTodoCounts } from '../state';
|
||||
import { colors } from '../theme';
|
||||
|
||||
const filters = [
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'active', label: 'Active' },
|
||||
{ key: 'completed', label: 'Done' },
|
||||
];
|
||||
|
||||
export default function FilterBar() {
|
||||
const { state, dispatch } = useTodoState();
|
||||
const counts = getTodoCounts(state);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{filters.map(({ key, label }) => {
|
||||
const isActive = state.filter === key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={key}
|
||||
onPress={() => dispatch({ type: 'SET_FILTER', filter: key })}
|
||||
style={[
|
||||
styles.pill,
|
||||
{ backgroundColor: isActive ? colors.primary : colors.border },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
{ color: isActive ? colors.onPrimary : colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{label} ({counts[key] || 0})
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
gap: 8,
|
||||
},
|
||||
pill: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
label: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, Pressable, FlatList, StyleSheet } from 'react-native';
|
||||
import { useTodoState, getFilteredTodos } from '../state';
|
||||
import { colors } from '../theme';
|
||||
import FilterBar from './FilterBar';
|
||||
import TodoItem from './TodoItem';
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyIcon}>{'\uD83D\uDCDD'}</Text>
|
||||
<Text style={styles.emptyText}>{'No todos yet!\nTap + to add one'}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function TodoList() {
|
||||
const { state } = useTodoState();
|
||||
const todos = getFilteredTodos(state);
|
||||
|
||||
if (todos.length === 0) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={todos}
|
||||
renderItem={({ item }) => <TodoItem todo={item} />}
|
||||
keyExtractor={item => String(item.id)}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MainScreen() {
|
||||
const { dispatch } = useTodoState();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerText}>Todos – JS Expo</Text>
|
||||
</View>
|
||||
<FilterBar />
|
||||
<View style={{ flex: 1 }}>
|
||||
<TodoList />
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={() => dispatch({ type: 'SHOW_ADD_FORM' })}
|
||||
style={styles.fab}
|
||||
>
|
||||
<Text style={styles.fabText}>+</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingTop: 48,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: colors.onPrimary,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
right: 24,
|
||||
bottom: 32,
|
||||
zIndex: 10,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
elevation: 6,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
fabText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 28,
|
||||
lineHeight: 30,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 48,
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 48,
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 18,
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
import { priorities } from '../state';
|
||||
|
||||
export default function PriorityPicker({ selected, onSelect }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{priorities.map(({ key, label, color }) => {
|
||||
const isSelected = key === selected;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={key}
|
||||
onPress={() => onSelect(key)}
|
||||
style={[
|
||||
styles.pill,
|
||||
{
|
||||
borderColor: color,
|
||||
backgroundColor: isSelected ? color : 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? '#FFFFFF' : color,
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginVertical: 8,
|
||||
},
|
||||
pill: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderWidth: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useTodoState } from '../state';
|
||||
import { colors } from '../theme';
|
||||
import * as db from '../db';
|
||||
import CategoryPicker from './CategoryPicker';
|
||||
import PriorityPicker from './PriorityPicker';
|
||||
|
||||
export default function TodoForm() {
|
||||
const { state, dispatch } = useTodoState();
|
||||
const editing = state.editingTodo;
|
||||
|
||||
const [title, setTitle] = useState(editing?.title || '');
|
||||
const [category, setCategory] = useState(editing?.category || 'Personal');
|
||||
const [priority, setPriority] = useState(editing?.priority || 'medium');
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
if (editing) {
|
||||
dispatch({
|
||||
type: 'UPDATE_TODO',
|
||||
id: editing.id,
|
||||
title: trimmed,
|
||||
category,
|
||||
priority,
|
||||
});
|
||||
db.updateTodo(editing.id, trimmed, category, priority).catch(err =>
|
||||
console.error('Update error:', err)
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const id = await db.insertTodo(trimmed, category, priority);
|
||||
dispatch({
|
||||
type: 'TODO_INSERTED',
|
||||
id,
|
||||
title: trimmed,
|
||||
category,
|
||||
priority,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Insert error:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
dispatch({ type: 'HIDE_FORM' });
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.heading}>
|
||||
{editing ? 'Edit Todo' : 'Add New Todo'}
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="What needs to be done?"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Text style={styles.sectionLabel}>Category</Text>
|
||||
<CategoryPicker selected={category} onSelect={setCategory} />
|
||||
|
||||
<Text style={[styles.sectionLabel, { marginTop: 8 }]}>Priority</Text>
|
||||
<PriorityPicker selected={priority} onSelect={setPriority} />
|
||||
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity onPress={handleCancel} style={styles.cancelBtn}>
|
||||
<Text style={{ color: colors.textSecondary, fontSize: 16 }}>
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}>
|
||||
<Text style={styles.submitText}>
|
||||
{editing ? 'Save' : 'Add'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
elevation: 8,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: colors.onSurface,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 4,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
marginTop: 24,
|
||||
},
|
||||
cancelBtn: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
},
|
||||
submitBtn: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
submitText: {
|
||||
color: colors.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { useTodoState } from '../state';
|
||||
import { colors, priorityColors } from '../theme';
|
||||
import * as db from '../db';
|
||||
|
||||
export default function TodoItem({ todo }) {
|
||||
const { dispatch } = useTodoState();
|
||||
const { id, title, completed, category, priority } = todo;
|
||||
|
||||
const handleToggle = () => {
|
||||
dispatch({ type: 'TOGGLE_TODO', id });
|
||||
db.toggleTodo(id, !completed).catch(err =>
|
||||
console.error('Toggle error:', err)
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
dispatch({ type: 'SHOW_EDIT_FORM', todo });
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch({ type: 'DELETE_TODO', id });
|
||||
db.deleteTodo(id).catch(err => console.error('Delete error:', err));
|
||||
};
|
||||
|
||||
const renderRightActions = () => (
|
||||
<TouchableOpacity onPress={handleDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.deleteText}>Delete</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<Swipeable renderRightActions={renderRightActions} overshootRight={false}>
|
||||
<TouchableOpacity
|
||||
onPress={handleEdit}
|
||||
activeOpacity={0.7}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: completed ? colors.completedBg : colors.surface,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity onPress={handleToggle} style={[
|
||||
styles.checkbox,
|
||||
{
|
||||
borderColor: completed ? colors.primary : colors.border,
|
||||
backgroundColor: completed ? colors.primary : 'transparent',
|
||||
},
|
||||
]}>
|
||||
{completed && <Text style={styles.checkmark}>{'\u2713'}</Text>}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.content}>
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: completed ? colors.completedText : colors.onSurface,
|
||||
textDecorationLine: completed ? 'line-through' : 'none',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={styles.meta}>
|
||||
<View style={styles.categoryBadge}>
|
||||
<Text style={styles.categoryText}>{category}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.priorityDot,
|
||||
{
|
||||
backgroundColor:
|
||||
priorityColors[priority] || '#999',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 4,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
checkbox: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
checkmark: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
gap: 8,
|
||||
},
|
||||
categoryBadge: {
|
||||
backgroundColor: '#E0E0E0',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 12,
|
||||
color: '#666666',
|
||||
},
|
||||
priorityDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#B00020',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
borderRadius: 12,
|
||||
marginVertical: 4,
|
||||
marginRight: 16,
|
||||
},
|
||||
deleteText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
|
||||
let db = null;
|
||||
|
||||
export async function openDb() {
|
||||
db = await SQLite.openDatabaseAsync('todos.db');
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function initDb() {
|
||||
if (!db) await openDb();
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
completed INTEGER DEFAULT 0,
|
||||
category TEXT DEFAULT 'Personal',
|
||||
priority TEXT DEFAULT 'medium',
|
||||
created_at INTEGER DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
export async function getAllTodos() {
|
||||
if (!db) return [];
|
||||
const rows = await db.getAllAsync('SELECT * FROM todos ORDER BY created_at DESC');
|
||||
return rows.map(row => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
completed: row.completed === 1,
|
||||
category: row.category,
|
||||
priority: row.priority,
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function insertTodo(title, category, priority) {
|
||||
if (!db) return null;
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO todos (title, category, priority) VALUES (?, ?, ?)',
|
||||
[title, category, priority]
|
||||
);
|
||||
return result.lastInsertRowId;
|
||||
}
|
||||
|
||||
export async function updateTodo(id, title, category, priority) {
|
||||
if (!db) return;
|
||||
await db.runAsync(
|
||||
'UPDATE todos SET title = ?, category = ?, priority = ? WHERE id = ?',
|
||||
[title, category, priority, id]
|
||||
);
|
||||
}
|
||||
|
||||
export async function toggleTodo(id, completed) {
|
||||
if (!db) return;
|
||||
await db.runAsync(
|
||||
'UPDATE todos SET completed = ? WHERE id = ?',
|
||||
[completed ? 1 : 0, id]
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTodo(id) {
|
||||
if (!db) return;
|
||||
await db.runAsync('DELETE FROM todos WHERE id = ?', [id]);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
(ns expo.root
|
||||
(:require
|
||||
["expo" :as expo]
|
||||
["create-react-class" :as crc]))
|
||||
|
||||
(defonce root-ref (atom nil))
|
||||
(defonce root-component-ref (atom nil))
|
||||
|
||||
(defn render-root [root]
|
||||
(let [first-call? (nil? @root-ref)]
|
||||
(reset! root-ref root)
|
||||
|
||||
(if-not first-call?
|
||||
(when-let [root @root-component-ref]
|
||||
(.forceUpdate ^js root))
|
||||
(let [Root
|
||||
(crc
|
||||
#js {:componentDidMount
|
||||
(fn []
|
||||
(this-as this
|
||||
(reset! root-component-ref this)))
|
||||
:componentWillUnmount
|
||||
(fn []
|
||||
(reset! root-component-ref nil))
|
||||
:render
|
||||
(fn []
|
||||
(let [body @root-ref]
|
||||
(if (fn? body)
|
||||
(body)
|
||||
body)))})]
|
||||
|
||||
(expo/registerRootComponent Root)))))
|
||||
@@ -1,27 +0,0 @@
|
||||
(ns todo.app
|
||||
(:require [reagent.core :as r]
|
||||
[re-frame.core :as rf]
|
||||
["react-native" :as rn]
|
||||
["react-native-gesture-handler" :refer [GestureHandlerRootView]]
|
||||
[expo.root :as expo-root]
|
||||
[todo.events]
|
||||
[todo.subs]
|
||||
[todo.views.main :as main]
|
||||
[todo.views.add-todo :as add-todo]))
|
||||
|
||||
(defn root []
|
||||
(let [show-form? @(rf/subscribe [:show-form?])]
|
||||
[:> GestureHandlerRootView {:style {:flex 1}}
|
||||
[:> rn/StatusBar {:bar-style "light-content" :background-color "#6200EE"}]
|
||||
[main/main-screen]
|
||||
(when show-form?
|
||||
[add-todo/add-todo-form])]))
|
||||
|
||||
(defn start
|
||||
{:dev/after-load true}
|
||||
[]
|
||||
(expo-root/render-root (r/as-element [root])))
|
||||
|
||||
(defn init []
|
||||
(rf/dispatch-sync [:initialize-db])
|
||||
(start))
|
||||
@@ -1,14 +0,0 @@
|
||||
(ns todo.db)
|
||||
|
||||
(def categories ["Personal" "Work" "Shopping" "Health" "Other"])
|
||||
|
||||
(def priorities [{:key :low :label "Low" :color "#4CAF50"}
|
||||
{:key :medium :label "Medium" :color "#FF9800"}
|
||||
{:key :high :label "High" :color "#F44336"}])
|
||||
|
||||
(def default-db
|
||||
{:todos {} ;; map of id -> todo
|
||||
:filter :all ;; :all | :active | :completed
|
||||
:next-temp-id -1 ;; for optimistic inserts before SQLite returns id
|
||||
:editing-todo nil ;; nil or todo map being edited
|
||||
:show-form? false}) ;; whether add/edit form is visible
|
||||
@@ -1,92 +0,0 @@
|
||||
(ns todo.events
|
||||
(:require [clojure.string :as str]
|
||||
[re-frame.core :as rf]
|
||||
[todo.db :as db]
|
||||
[todo.fx]))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:initialize-db
|
||||
(fn [_ _]
|
||||
{:db db/default-db
|
||||
:sqlite/init true}))
|
||||
|
||||
(rf/reg-event-db
|
||||
:todos-loaded
|
||||
(fn [db [_ rows]]
|
||||
(let [todos (->> rows
|
||||
(map (fn [row]
|
||||
(let [id (:id row)]
|
||||
[id {:id id
|
||||
:title (:title row)
|
||||
:completed (= 1 (:completed row))
|
||||
:category (:category row)
|
||||
:priority (keyword (:priority row))
|
||||
:created-at (:created_at row)}])))
|
||||
(into {}))]
|
||||
(assoc db :todos todos))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:add-todo
|
||||
(fn [{:keys [db]} [_ title category priority]]
|
||||
(when (seq (str/trim (or title "")))
|
||||
{:db (assoc db :show-form? false)
|
||||
:sqlite/insert {:title (str/trim title)
|
||||
:category category
|
||||
:priority (name priority)
|
||||
:on-success [:todo-inserted title category priority]}})))
|
||||
|
||||
(rf/reg-event-db
|
||||
:todo-inserted
|
||||
(fn [db [_ title category priority id]]
|
||||
(assoc-in db [:todos id]
|
||||
{:id id
|
||||
:title (str/trim title)
|
||||
:completed false
|
||||
:category category
|
||||
:priority (keyword priority)
|
||||
:created-at (js/Math.floor (/ (.now js/Date) 1000))})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:toggle-todo
|
||||
(fn [{:keys [db]} [_ id]]
|
||||
(let [todo (get-in db [:todos id])
|
||||
new-completed (not (:completed todo))]
|
||||
{:db (assoc-in db [:todos id :completed] new-completed)
|
||||
:sqlite/toggle {:id id :completed new-completed}})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:update-todo
|
||||
(fn [{:keys [db]} [_ id title category priority]]
|
||||
{:db (-> db
|
||||
(assoc-in [:todos id :title] title)
|
||||
(assoc-in [:todos id :category] category)
|
||||
(assoc-in [:todos id :priority] priority)
|
||||
(assoc :show-form? false
|
||||
:editing-todo nil))
|
||||
:sqlite/update {:id id :title title :category category :priority (name priority)}}))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:delete-todo
|
||||
(fn [{:keys [db]} [_ id]]
|
||||
{:db (update db :todos dissoc id)
|
||||
:sqlite/delete {:id id}}))
|
||||
|
||||
(rf/reg-event-db
|
||||
:set-filter
|
||||
(fn [db [_ filter-val]]
|
||||
(assoc db :filter filter-val)))
|
||||
|
||||
(rf/reg-event-db
|
||||
:show-add-form
|
||||
(fn [db _]
|
||||
(assoc db :show-form? true :editing-todo nil)))
|
||||
|
||||
(rf/reg-event-db
|
||||
:show-edit-form
|
||||
(fn [db [_ todo]]
|
||||
(assoc db :show-form? true :editing-todo todo)))
|
||||
|
||||
(rf/reg-event-db
|
||||
:hide-form
|
||||
(fn [db _]
|
||||
(assoc db :show-form? false :editing-todo nil)))
|
||||
@@ -1,49 +0,0 @@
|
||||
(ns todo.fx
|
||||
(:require [re-frame.core :as rf]
|
||||
[todo.sqlite :as sqlite]))
|
||||
|
||||
(defonce db-instance (atom nil))
|
||||
|
||||
(rf/reg-fx
|
||||
:sqlite/init
|
||||
(fn [_]
|
||||
(-> (sqlite/open-db)
|
||||
(.then (fn [db]
|
||||
(reset! db-instance db)
|
||||
(-> (sqlite/init-db db)
|
||||
(.then (fn [_]
|
||||
(-> (sqlite/get-all-todos db)
|
||||
(.then (fn [rows]
|
||||
(rf/dispatch [:todos-loaded (js->clj rows :keywordize-keys true)])))))))))
|
||||
(.catch (fn [err] (js/console.error "SQLite init error:" err))))))
|
||||
|
||||
(rf/reg-fx
|
||||
:sqlite/insert
|
||||
(fn [{:keys [title category priority on-success]}]
|
||||
(when-let [db @db-instance]
|
||||
(-> (sqlite/insert-todo db title category priority)
|
||||
(.then (fn [result]
|
||||
(when on-success
|
||||
(rf/dispatch (conj on-success (.-lastInsertRowId ^js result))))))
|
||||
(.catch (fn [err] (js/console.error "Insert error:" err)))))))
|
||||
|
||||
(rf/reg-fx
|
||||
:sqlite/update
|
||||
(fn [{:keys [id title category priority]}]
|
||||
(when-let [db @db-instance]
|
||||
(-> (sqlite/update-todo-title db id title category priority)
|
||||
(.catch (fn [err] (js/console.error "Update error:" err)))))))
|
||||
|
||||
(rf/reg-fx
|
||||
:sqlite/toggle
|
||||
(fn [{:keys [id completed]}]
|
||||
(when-let [db @db-instance]
|
||||
(-> (sqlite/toggle-todo db id completed)
|
||||
(.catch (fn [err] (js/console.error "Toggle error:" err)))))))
|
||||
|
||||
(rf/reg-fx
|
||||
:sqlite/delete
|
||||
(fn [{:keys [id]}]
|
||||
(when-let [db @db-instance]
|
||||
(-> (sqlite/delete-todo db id)
|
||||
(.catch (fn [err] (js/console.error "Delete error:" err)))))))
|
||||
@@ -1,39 +0,0 @@
|
||||
(ns todo.sqlite
|
||||
(:require ["expo-sqlite" :as SQLite]))
|
||||
|
||||
(defn open-db []
|
||||
(SQLite/openDatabaseAsync "todos.db"))
|
||||
|
||||
(defn init-db [^js db]
|
||||
(.execAsync db
|
||||
"CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
completed INTEGER DEFAULT 0,
|
||||
category TEXT DEFAULT 'Personal',
|
||||
priority TEXT DEFAULT 'medium',
|
||||
created_at INTEGER DEFAULT (strftime('%s','now'))
|
||||
);"))
|
||||
|
||||
(defn get-all-todos [^js db]
|
||||
(.getAllAsync db "SELECT * FROM todos ORDER BY created_at DESC"))
|
||||
|
||||
(defn insert-todo [^js db title category priority]
|
||||
(.runAsync db
|
||||
"INSERT INTO todos (title, category, priority) VALUES (?, ?, ?)"
|
||||
#js [title category priority]))
|
||||
|
||||
(defn update-todo-title [^js db id title category priority]
|
||||
(.runAsync db
|
||||
"UPDATE todos SET title = ?, category = ?, priority = ? WHERE id = ?"
|
||||
#js [title category priority id]))
|
||||
|
||||
(defn toggle-todo [^js db id completed]
|
||||
(.runAsync db
|
||||
"UPDATE todos SET completed = ? WHERE id = ?"
|
||||
#js [(if completed 1 0) id]))
|
||||
|
||||
(defn delete-todo [^js db id]
|
||||
(.runAsync db
|
||||
"DELETE FROM todos WHERE id = ?"
|
||||
#js [id]))
|
||||
@@ -1,27 +0,0 @@
|
||||
(ns todo.subs
|
||||
(:require [re-frame.core :as rf]))
|
||||
|
||||
(rf/reg-sub :todos (fn [db _] (:todos db)))
|
||||
(rf/reg-sub :filter (fn [db _] (:filter db)))
|
||||
(rf/reg-sub :show-form? (fn [db _] (:show-form? db)))
|
||||
(rf/reg-sub :editing-todo (fn [db _] (:editing-todo db)))
|
||||
|
||||
(rf/reg-sub
|
||||
:filtered-todos
|
||||
:<- [:todos]
|
||||
:<- [:filter]
|
||||
(fn [[todos filter-val] _]
|
||||
(let [todo-list (sort-by :created-at > (vals todos))]
|
||||
(case filter-val
|
||||
:all todo-list
|
||||
:active (filter #(not (:completed %)) todo-list)
|
||||
:completed (filter :completed todo-list)))))
|
||||
|
||||
(rf/reg-sub
|
||||
:todo-counts
|
||||
:<- [:todos]
|
||||
(fn [todos _]
|
||||
(let [all (vals todos)]
|
||||
{:all (count all)
|
||||
:active (count (filter #(not (:completed %)) all))
|
||||
:completed (count (filter :completed all))})))
|
||||
@@ -1,24 +0,0 @@
|
||||
(ns todo.theme)
|
||||
|
||||
(def colors
|
||||
{:primary "#6200EE"
|
||||
:primary-dark "#3700B3"
|
||||
:secondary "#03DAC6"
|
||||
:background "#F5F5F5"
|
||||
:surface "#FFFFFF"
|
||||
:error "#B00020"
|
||||
:on-primary "#FFFFFF"
|
||||
:on-surface "#000000"
|
||||
:on-background "#000000"
|
||||
:text-secondary "#666666"
|
||||
:border "#E0E0E0"
|
||||
:completed-text "#999999"
|
||||
:completed-bg "#F0F0F0"})
|
||||
|
||||
(def spacing
|
||||
{:xs 4 :sm 8 :md 16 :lg 24 :xl 32})
|
||||
|
||||
(def priority-colors
|
||||
{:low "#4CAF50"
|
||||
:medium "#FF9800"
|
||||
:high "#F44336"})
|
||||
@@ -1,65 +0,0 @@
|
||||
(ns todo.views.add-todo
|
||||
(:require [clojure.string :as str]
|
||||
[reagent.core :as r]
|
||||
["react-native" :as rn]
|
||||
[re-frame.core :as rf]
|
||||
[todo.theme :as theme]
|
||||
[todo.views.category-picker :as picker]))
|
||||
|
||||
(defn add-todo-form []
|
||||
(let [editing @(rf/subscribe [:editing-todo])
|
||||
title (r/atom (or (:title editing) ""))
|
||||
category (r/atom (or (:category editing) "Personal"))
|
||||
priority (r/atom (or (:priority editing) :medium))]
|
||||
(fn []
|
||||
[:> rn/View {:style {:position "absolute" :top 0 :left 0 :right 0 :bottom 0
|
||||
:background-color "rgba(0,0,0,0.5)"
|
||||
:justify-content "center"
|
||||
:padding 24}}
|
||||
[:> rn/View {:style {:background-color (:surface theme/colors)
|
||||
:border-radius 16
|
||||
:padding 24
|
||||
:elevation 8}}
|
||||
[:> rn/Text {:style {:font-size 20 :font-weight "bold" :margin-bottom 16
|
||||
:color (:on-surface theme/colors)}}
|
||||
(if editing "Edit Todo" "Add New Todo")]
|
||||
|
||||
[:> rn/TextInput
|
||||
{:style {:border-width 1 :border-color (:border theme/colors)
|
||||
:border-radius 8 :padding 12 :font-size 16
|
||||
:margin-bottom 16}
|
||||
:placeholder "What needs to be done?"
|
||||
:placeholder-text-color (:text-secondary theme/colors)
|
||||
:value @title
|
||||
:on-change-text #(reset! title %)
|
||||
:auto-focus true}]
|
||||
|
||||
[:> rn/Text {:style {:font-size 14 :font-weight "600" :color (:text-secondary theme/colors)
|
||||
:margin-bottom 4}}
|
||||
"Category"]
|
||||
[picker/category-picker @category #(reset! category %)]
|
||||
|
||||
[:> rn/Text {:style {:font-size 14 :font-weight "600" :color (:text-secondary theme/colors)
|
||||
:margin-bottom 4 :margin-top 8}}
|
||||
"Priority"]
|
||||
[picker/priority-picker @priority #(reset! priority %)]
|
||||
|
||||
[:> rn/View {:style {:flex-direction "row" :justify-content "flex-end"
|
||||
:gap 12 :margin-top 24}}
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:hide-form])
|
||||
:style {:padding-horizontal 20 :padding-vertical 10
|
||||
:border-radius 8}}
|
||||
[:> rn/Text {:style {:color (:text-secondary theme/colors) :font-size 16}} "Cancel"]]
|
||||
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press (fn []
|
||||
(when (seq (str/trim @title))
|
||||
(if editing
|
||||
(rf/dispatch [:update-todo (:id editing) @title @category @priority])
|
||||
(rf/dispatch [:add-todo @title @category @priority]))))
|
||||
:style {:padding-horizontal 20 :padding-vertical 10
|
||||
:border-radius 8
|
||||
:background-color (:primary theme/colors)}}
|
||||
[:> rn/Text {:style {:color (:on-primary theme/colors) :font-size 16 :font-weight "600"}}
|
||||
(if editing "Save" "Add")]]]]])))
|
||||
@@ -1,40 +0,0 @@
|
||||
(ns todo.views.category-picker
|
||||
(:require [reagent.core :as r]
|
||||
["react-native" :as rn]
|
||||
[todo.db :as db]
|
||||
[todo.theme :as theme]))
|
||||
|
||||
(defn category-picker [selected on-select]
|
||||
[:> rn/View {:style {:flex-direction "row" :flex-wrap "wrap" :gap 8 :margin-vertical 8}}
|
||||
(for [cat db/categories]
|
||||
^{:key cat}
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(on-select cat)
|
||||
:style {:padding-horizontal 16
|
||||
:padding-vertical 8
|
||||
:border-radius 20
|
||||
:background-color (if (= cat selected)
|
||||
(:primary theme/colors)
|
||||
(:border theme/colors))}}
|
||||
[:> rn/Text {:style {:color (if (= cat selected)
|
||||
(:on-primary theme/colors)
|
||||
(:on-surface theme/colors))
|
||||
:font-size 14}}
|
||||
cat]])])
|
||||
|
||||
(defn priority-picker [selected on-select]
|
||||
[:> rn/View {:style {:flex-direction "row" :gap 8 :margin-vertical 8}}
|
||||
(for [{:keys [key label color]} db/priorities]
|
||||
^{:key key}
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(on-select key)
|
||||
:style {:padding-horizontal 16
|
||||
:padding-vertical 8
|
||||
:border-radius 20
|
||||
:border-width 2
|
||||
:border-color color
|
||||
:background-color (if (= key selected) color "transparent")}}
|
||||
[:> rn/Text {:style {:color (if (= key selected) "#FFFFFF" color)
|
||||
:font-weight "600"
|
||||
:font-size 14}}
|
||||
label]])])
|
||||
@@ -1,72 +0,0 @@
|
||||
(ns todo.views.main
|
||||
(:require [reagent.core :as r]
|
||||
["react-native" :as rn]
|
||||
[re-frame.core :as rf]
|
||||
[todo.theme :as theme]
|
||||
[todo.views.todo-item :as todo-item]))
|
||||
|
||||
(defn filter-bar []
|
||||
(let [current-filter @(rf/subscribe [:filter])
|
||||
counts @(rf/subscribe [:todo-counts])]
|
||||
[:> rn/View {:style {:flex-direction "row" :padding-horizontal 16
|
||||
:padding-vertical 8 :gap 8}}
|
||||
(for [[filter-key label] [[:all "All"] [:active "Active"] [:completed "Done"]]]
|
||||
^{:key filter-key}
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:set-filter filter-key])
|
||||
:style {:padding-horizontal 16 :padding-vertical 8
|
||||
:border-radius 20
|
||||
:background-color (if (= current-filter filter-key)
|
||||
(:primary theme/colors)
|
||||
(:border theme/colors))}}
|
||||
[:> rn/Text {:style {:color (if (= current-filter filter-key)
|
||||
(:on-primary theme/colors)
|
||||
(:text-secondary theme/colors))
|
||||
:font-weight "600"}}
|
||||
(str label " (" (get counts filter-key 0) ")")]])]))
|
||||
|
||||
(defn empty-state []
|
||||
[:> rn/View {:style {:flex 1 :justify-content "center" :align-items "center" :padding 48}}
|
||||
[:> rn/Text {:style {:font-size 48 :margin-bottom 16}} "\uD83D\uDCDD"]
|
||||
[:> rn/Text {:style {:font-size 18 :color (:text-secondary theme/colors)
|
||||
:text-align "center"}}
|
||||
"No todos yet!\nTap + to add one"]])
|
||||
|
||||
(defn todo-list []
|
||||
(let [todos @(rf/subscribe [:filtered-todos])]
|
||||
(if (empty? todos)
|
||||
[empty-state]
|
||||
[:> rn/FlatList
|
||||
{:data (clj->js (map #(assoc % :key (str (:id %))) todos))
|
||||
:render-item (fn [^js item]
|
||||
(let [data (js->clj (.-item item) :keywordize-keys true)
|
||||
data (update data :priority keyword)]
|
||||
(r/as-element [todo-item/todo-item data])))
|
||||
:key-extractor (fn [^js item] (str (.-id item)))
|
||||
:content-container-style {:padding-bottom 100}}])))
|
||||
|
||||
(defn fab []
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:show-add-form])
|
||||
:style {:position "absolute"
|
||||
:right 24 :bottom 32
|
||||
:width 56 :height 56 :border-radius 28
|
||||
:background-color (:primary theme/colors)
|
||||
:justify-content "center" :align-items "center"
|
||||
:elevation 6
|
||||
:shadow-color "#000"
|
||||
:shadow-offset {:width 0 :height 3}
|
||||
:shadow-opacity 0.3
|
||||
:shadow-radius 4}}
|
||||
[:> rn/Text {:style {:color "#FFFFFF" :font-size 28 :line-height 30}} "+"]])
|
||||
|
||||
(defn main-screen []
|
||||
[:> rn/View {:style {:flex 1 :background-color (:background theme/colors)}}
|
||||
;; Header
|
||||
[:> rn/View {:style {:background-color (:primary theme/colors)
|
||||
:padding-top 48 :padding-bottom 16 :padding-horizontal 24}}
|
||||
[:> rn/Text {:style {:font-size 28 :font-weight "bold" :color (:on-primary theme/colors)}}
|
||||
"My Todos"]]
|
||||
[filter-bar]
|
||||
[todo-list]
|
||||
[fab]])
|
||||
@@ -1,64 +0,0 @@
|
||||
(ns todo.views.todo-item
|
||||
(:require [reagent.core :as r]
|
||||
["react-native" :as rn]
|
||||
["react-native-gesture-handler" :refer [Swipeable]]
|
||||
[re-frame.core :as rf]
|
||||
[todo.theme :as theme]))
|
||||
|
||||
(defn render-right-actions [id]
|
||||
(fn [_progress _drag-x]
|
||||
(r/as-element
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:delete-todo id])
|
||||
:style {:background-color (:error theme/colors)
|
||||
:justify-content "center"
|
||||
:align-items "center"
|
||||
:width 80
|
||||
:border-radius 12
|
||||
:margin-vertical 4
|
||||
:margin-right 16}}
|
||||
[:> rn/Text {:style {:color "#FFFFFF" :font-weight "bold" :font-size 14}} "Delete"]])))
|
||||
|
||||
(defn todo-item [{:keys [id title completed category priority]}]
|
||||
[:> Swipeable {:render-right-actions (render-right-actions id)
|
||||
:overshoot-right false}
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:show-edit-form {:id id :title title :completed completed
|
||||
:category category :priority priority}])
|
||||
:active-opacity 0.7
|
||||
:style {:flex-direction "row"
|
||||
:align-items "center"
|
||||
:background-color (if completed (:completed-bg theme/colors) (:surface theme/colors))
|
||||
:margin-horizontal 16
|
||||
:margin-vertical 4
|
||||
:padding 16
|
||||
:border-radius 12
|
||||
:elevation 2
|
||||
:shadow-color "#000"
|
||||
:shadow-offset {:width 0 :height 1}
|
||||
:shadow-opacity 0.1
|
||||
:shadow-radius 2}}
|
||||
|
||||
[:> rn/TouchableOpacity
|
||||
{:on-press #(rf/dispatch [:toggle-todo id])
|
||||
:style {:width 28 :height 28 :border-radius 14
|
||||
:border-width 2
|
||||
:border-color (if completed (:primary theme/colors) (:border theme/colors))
|
||||
:background-color (if completed (:primary theme/colors) "transparent")
|
||||
:justify-content "center" :align-items "center"
|
||||
:margin-right 12}}
|
||||
(when completed
|
||||
[:> rn/Text {:style {:color "#FFFFFF" :font-size 16}} "\u2713"])]
|
||||
|
||||
[:> rn/View {:style {:flex 1}}
|
||||
[:> rn/Text {:style {:font-size 16
|
||||
:color (if completed (:completed-text theme/colors) (:on-surface theme/colors))
|
||||
:text-decoration-line (if completed "line-through" "none")}}
|
||||
title]
|
||||
[:> rn/View {:style {:flex-direction "row" :align-items "center" :margin-top 4 :gap 8}}
|
||||
[:> rn/View {:style {:background-color (:border theme/colors)
|
||||
:padding-horizontal 8 :padding-vertical 2
|
||||
:border-radius 10}}
|
||||
[:> rn/Text {:style {:font-size 12 :color (:text-secondary theme/colors)}} category]]
|
||||
[:> rn/View {:style {:width 10 :height 10 :border-radius 5
|
||||
:background-color (get theme/priority-colors priority "#999")}}]]]]])
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
export const categories = ['Personal', 'Work', 'Shopping', 'Health', 'Other'];
|
||||
|
||||
export const priorities = [
|
||||
{ key: 'low', label: 'Low', color: '#4CAF50' },
|
||||
{ key: 'medium', label: 'Medium', color: '#FF9800' },
|
||||
{ key: 'high', label: 'High', color: '#F44336' },
|
||||
];
|
||||
|
||||
const initialState = {
|
||||
todos: {},
|
||||
filter: 'all',
|
||||
editingTodo: null,
|
||||
showForm: false,
|
||||
};
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'TODOS_LOADED': {
|
||||
const todos = {};
|
||||
action.rows.forEach(row => {
|
||||
todos[row.id] = row;
|
||||
});
|
||||
return { ...state, todos };
|
||||
}
|
||||
case 'TODO_INSERTED': {
|
||||
const { id, title, category, priority } = action;
|
||||
return {
|
||||
...state,
|
||||
showForm: false,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[id]: {
|
||||
id,
|
||||
title,
|
||||
completed: false,
|
||||
category,
|
||||
priority,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'TOGGLE_TODO': {
|
||||
const todo = state.todos[action.id];
|
||||
if (!todo) return state;
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: { ...todo, completed: !todo.completed },
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'UPDATE_TODO': {
|
||||
const { id, title, category, priority } = action;
|
||||
const existing = state.todos[id];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
...state,
|
||||
showForm: false,
|
||||
editingTodo: null,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[id]: { ...existing, title, category, priority },
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'DELETE_TODO': {
|
||||
const { [action.id]: _, ...rest } = state.todos;
|
||||
return { ...state, todos: rest };
|
||||
}
|
||||
case 'SET_FILTER':
|
||||
return { ...state, filter: action.filter };
|
||||
case 'SHOW_ADD_FORM':
|
||||
return { ...state, showForm: true, editingTodo: null };
|
||||
case 'SHOW_EDIT_FORM':
|
||||
return { ...state, showForm: true, editingTodo: action.todo };
|
||||
case 'HIDE_FORM':
|
||||
return { ...state, showForm: false, editingTodo: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const TodoContext = createContext(null);
|
||||
|
||||
export function TodoProvider({ children }) {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
return (
|
||||
<TodoContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</TodoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTodoState() {
|
||||
return useContext(TodoContext);
|
||||
}
|
||||
|
||||
export function getFilteredTodos(state) {
|
||||
const todoList = Object.values(state.todos).sort(
|
||||
(a, b) => (b.createdAt || 0) - (a.createdAt || 0)
|
||||
);
|
||||
switch (state.filter) {
|
||||
case 'active':
|
||||
return todoList.filter(t => !t.completed);
|
||||
case 'completed':
|
||||
return todoList.filter(t => t.completed);
|
||||
default:
|
||||
return todoList;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTodoCounts(state) {
|
||||
const all = Object.values(state.todos);
|
||||
return {
|
||||
all: all.length,
|
||||
active: all.filter(t => !t.completed).length,
|
||||
completed: all.filter(t => t.completed).length,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const colors = {
|
||||
primary: '#6200EE',
|
||||
primaryDark: '#3700B3',
|
||||
secondary: '#03DAC6',
|
||||
background: '#F5F5F5',
|
||||
surface: '#FFFFFF',
|
||||
error: '#B00020',
|
||||
onPrimary: '#FFFFFF',
|
||||
onSurface: '#000000',
|
||||
onBackground: '#000000',
|
||||
textSecondary: '#666666',
|
||||
border: '#E0E0E0',
|
||||
completedText: '#999999',
|
||||
completedBg: '#F0F0F0',
|
||||
};
|
||||
|
||||
export const priorityColors = {
|
||||
low: '#4CAF50',
|
||||
medium: '#FF9800',
|
||||
high: '#F44336',
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
};
|
||||
Reference in New Issue
Block a user