Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/daily_planner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: daily_planner

on:
push:
branches:
- master
pull_request:
paths:
- motoko/daily_planner/**
- .github/workflows/daily_planner.yml

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
motoko-daily_planner:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: motoko/daily_planner
run: |
icp network start -d
icp deploy
make test
20 changes: 0 additions & 20 deletions motoko/daily_planner/.devcontainer/devcontainer.json

This file was deleted.

113 changes: 0 additions & 113 deletions motoko/daily_planner/BUILD.md

This file was deleted.

39 changes: 39 additions & 0 deletions motoko/daily_planner/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.PHONY: test

test:
@echo "=== Test 1/6: getDayData returns null for a date with no notes ==="
@result=$$(icp canister call backend getDayData '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'null' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 2/6: addNote returns ok result ==="
@result=$$(icp canister call backend addNote '("2000-01-15", "Buy groceries")') && \
echo "$$result" && \
echo "$$result" | grep -q 'ok' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 3/6: getDayData returns stored note ==="
@result=$$(icp canister call backend getDayData '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'Buy groceries' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 4/6: getMonthData returns entry for the stored month ==="
@result=$$(icp canister call backend getMonthData '(2000, 1)') && \
echo "$$result" && \
echo "$$result" | grep -q 'Buy groceries' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 5/6: completeNote marks note as completed ==="
@icp canister call backend completeNote '("2000-01-15", 0)' && \
result=$$(icp canister call backend getDayData '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'isCompleted = true' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 6/6: getMonthData returns empty list for a different month ==="
@result=$$(icp canister call backend getMonthData '(1999, 12)') && \
echo "$$result" && \
echo "$$result" | grep -q 'vec {}' && \
echo "PASS" || (echo "FAIL" && exit 1)
39 changes: 28 additions & 11 deletions motoko/daily_planner/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
# Daily planner
# Daily Planner

Daily planner features a monthly calender that can be used to track daily activities, appointments, or tasks. Data for each task is stored onchain. For each day, a historic fact can be queried using HTTPS outcalls, which is a feature that allows ICP canisters to obtain data from external sources.
Daily Planner is a full-stack ICP example featuring a monthly calendar that tracks daily notes and tasks stored on the network. For each day, a historic fact can be fetched from an external API using HTTPS outcalls, demonstrating how ICP canisters can access data from external services.

## Deploying from ICP Ninja
## Build and deploy from the command line

[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/daily_planner)
### Prerequisites

## Build and deploy from the command-line
- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`

### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)
### Install

### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)
```bash
git clone https://github.com/dfinity/examples
cd examples/motoko/daily_planner
```

### Deploy and test

### 3. Navigate into the project's directory.
```bash
icp network start -d
icp deploy
make test
icp network stop
```

### 4. Deploy the project to your local environment:
To run the frontend in development mode with hot reload:

```bash
npm run dev
```
dfx start --background --clean && dfx deploy

## Updating the Candid interface

```bash
$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo
```

## Security considerations and best practices

If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.
21 changes: 11 additions & 10 deletions motoko/daily_planner/backend/app.mo
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import Result "mo:core/Result";
import JSON "mo:json";
import HashMap "mo:map/Map";
import { thash } "mo:map/Map";
import IC "ic:aaaaa-aa";
import { ic } "mo:ic";
import IC "mo:ic/Types";

persistent actor DailyPlanner {
// General types used by the planner
Expand Down Expand Up @@ -43,9 +44,9 @@ persistent actor DailyPlanner {
// Query function to get data for an entire month.
// Returns a
public query func getMonthData(year : Nat, month : Nat) : async [(Text, DayData)] {
let monthPrefix = year.toText() # "-" # month.toText() # "-";
Iter.filter(
HashMap.entries(dayData),
let monthStr = if (month < 10) { "0" # month.toText() } else { month.toText() };
let monthPrefix = year.toText() # "-" # monthStr # "-";
HashMap.entries(dayData).filter(
func((k, _) : (Text, DayData)) : Bool {
k.startsWith(#text monthPrefix);
},
Expand Down Expand Up @@ -121,9 +122,9 @@ persistent actor DailyPlanner {
// "transform" is used to specify how the HTTP response is processed before consensus tries to agree on a response.
// This is useful to e.g. filter out timestamps out of headers that will be different across the responses the different replicas receive.
// You can read more about it here: https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-how-to-use
let http_request : IC.http_request_args = {
let http_request : IC.HttpRequestArgs = {
// API must support IPv6
url = "https://byabbe.se/on-this-day/" # Nat.toText(month) # "/" # Nat.toText(day) # "/events.json";
url = "https://byabbe.se/on-this-day/" # month.toText() # "/" # day.toText() # "/events.json";
max_response_bytes = null; //optional for request
headers = [];
body = null; //optional for request
Expand All @@ -138,10 +139,10 @@ persistent actor DailyPlanner {
// Perform HTTPS outcall using roughly 100B cycles.
// See https outcall cost calculator: https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io.
// Unused cycles are returned.
let http_response : IC.http_request_result = await (with cycles = 100_000_000_000) IC.http_request(http_request);
let http_response : IC.HttpRequestResult = await (with cycles = 100_000_000_000) ic.http_request(http_request);

// Parse response into JSON
let decoded_text : Text = switch (Text.decodeUtf8(http_response.body)) {
let decoded_text : Text = switch (http_response.body.decodeUtf8()) {
case (null) { "No value returned" };
case (?y) { y };
};
Expand Down Expand Up @@ -186,8 +187,8 @@ persistent actor DailyPlanner {
// Transforms the raw HTTPS call response to an HttpResponsePayload on which the nodes can run consensus on.
public query func transform({
context : Blob;
response : IC.http_request_result;
}) : async IC.http_request_result {
response : IC.HttpRequestResult;
}) : async IC.HttpRequestResult {
{
response with headers = []; // not interested in the headers
};
Expand Down
27 changes: 27 additions & 0 deletions motoko/daily_planner/backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type Note = record {
id : nat;
content : text;
isCompleted : bool;
};

type OnThisDay = record {
title : text;
year : text;
wikiLink : text;
};

type DayData = record {
notes : vec Note;
onThisDay : opt OnThisDay;
};

type AddNoteResult = variant { ok : text; err : text };

service : {
getDayData : (date : text) -> (opt DayData) query;
getMonthData : (year : nat, month : nat) -> (vec record { text; DayData }) query;
addNote : (date : text, content : text) -> (AddNoteResult);
completeNote : (date : text, noteId : nat) -> ();
fetchAndStoreOnThisDay : (date : text) -> (variant { ok : text; err : text });
transform : (record { context : blob; response : record { status : nat; headers : vec record { name : text; value : text }; body : blob } }) -> (record { status : nat; headers : vec record { name : text; value : text }; body : blob }) query;
};
23 changes: 0 additions & 23 deletions motoko/daily_planner/dfx.json

This file was deleted.

6 changes: 3 additions & 3 deletions motoko/daily_planner/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"prebuild": "npm i --include=dev && dfx generate backend",
"prebuild": "npm i --include=dev",
"build": "vite build",
"dev": "vite"
},
Expand All @@ -13,10 +13,10 @@
"react-dom": "18.3.1"
},
"devDependencies": {
"@icp-sdk/bindgen": "~0.2.2",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.3",
"vite": "5.4.11",
"vite-plugin-environment": "1.1.3"
"vite": "5.4.11"
}
}
2 changes: 1 addition & 1 deletion motoko/daily_planner/frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import '../index.css';
import { backend } from 'declarations/backend';
import { backend } from './actor';

const App = () => {
const [currentDate, setCurrentDate] = useState(new Date());
Expand Down
Loading
Loading