Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
18b94f4
[ADD] Discover the JavaScript framework
ged-odoo Oct 9, 2022
5b999b0
[SOLUTION] 1.1 Displaying a counter
ged-odoo May 10, 2023
483b01b
[SOLUTION] 1.2 Extract counter in a sub component
ged-odoo May 10, 2023
d55533a
[Solution] 1.3 A simple Card component
ged-odoo May 10, 2023
4481e46
[Solution] 1.4 Using markup to display html
ged-odoo May 10, 2023
b03addb
[Solution] 1.5 Props validation
ged-odoo May 10, 2023
180bbc2
[Solution] 1.6 The sum of two Counter
ged-odoo May 10, 2023
5772656
[Solution] 1.7 A todo list
ged-odoo May 10, 2023
2e9c9d7
[Solution] 1.8 Use dynamic attribute
ged-odoo May 10, 2023
558c4f1
[Solution] 1.9 Adding a todo
ged-odoo May 10, 2023
313518d
[Solution] 1.10 Focusing the input
ged-odoo May 10, 2023
1297916
[Solution] 1.11 Toggling todos
ged-odoo May 10, 2023
69f8d86
[Solution] 1.12 Deleting todos
ged-odoo May 10, 2023
2017aff
[Solution] 1.13 Generic card with slots
ged-odoo May 10, 2023
3d3e7c8
[Solution] 1.14 Minimizing card content
ged-odoo May 10, 2023
ae5b825
[Solution] 2.1 A new Layout
ged-odoo May 10, 2023
92bd224
[Solution] 2.2 Add some buttons for quick navigation
ged-odoo May 10, 2023
68212f6
[Solution] 2.3 Add a DashboardItem
ged-odoo May 10, 2023
de2ca57
[Solution] 2.4 Call the server, add some statistics
ged-odoo May 10, 2023
f4ffaac
[Solution] 2.5 Cache network calls, create a service
ged-odoo May 10, 2023
7dc93c6
[Solution] 2.6 Display a pie chart
ged-odoo May 10, 2023
c8416e2
[Solution] 2.7 Real life update
ged-odoo May 10, 2023
b9c09ae
[Solution] 2.8 Lazy loading the dashboard
fdardenne Jun 9, 2023
f11304a
[Solution] 2.9 Making our dashboard generic
fdardenne Jun 9, 2023
7540452
[Solution] 2.10 Making our dashboard extensible
fdardenne Jun 9, 2023
d7d44a1
[Solution] 2.11 Add and remove dashboard items
fdardenne Jun 9, 2023
e421221
[Solution] 3.1 Create a systray item
fdardenne Aug 16, 2023
2fc331e
[Solution] 3.2 Count external clicks
fdardenne Aug 16, 2023
d8c6ce8
[Solution] 3.3 Create a client action
fdardenne Aug 16, 2023
9325593
[Solution] 3.4 Move the state to a service
fdardenne Aug 16, 2023
8d17775
[Solution] 3.5 Use a custom hook
fdardenne Oct 9, 2023
8946523
[Solution] 3.6 Humanize the displayed value
fdardenne Sep 28, 2023
ab573c9
[Solution] 3.7 Add a tooltip in ClickValue component
fdardenne Oct 9, 2023
b54d8a5
[Solution] 3.8 Buy ClickBots
fdardenne Sep 29, 2023
7ab2433
[Solution] 3.9 Refactor to a class model
fdardenne Oct 10, 2023
95aa12e
[Solution] 3.10 Notify when a milestone is reached
fdardenne Sep 29, 2023
5319268
Solution] 3.11 Add BigBots
fdardenne Sep 29, 2023
cf18b0a
[Solution] 3.12 Add a new type of resource: power
fdardenne Sep 29, 2023
496878b
[Solution] 3.13 Define some random rewards
fdardenne Oct 2, 2023
d78c12b
[Solution] 3.14 Provide a reward when opening a form view
fdardenne Oct 2, 2023
fbaf0da
[Solution] 3.15 Add commands in command palette
fdardenne Oct 2, 2023
3b0ecff
[Solution] 3.16 Add yet another resource: trees
fdardenne Oct 2, 2023
4249878
[Solution] 3.17 Use a dropdown menu for the systray item
fdardenne Oct 3, 2023
f4a5071
[Solution] 3.18 Use a Notebook component
fdardenne Oct 2, 2023
cea4fde
[Solution] 3.19 Persist the game state
fdardenne Oct 3, 2023
b7d16ea
[Solution] 3.20 Introduce state migration system
fdardenne Oct 3, 2023
b5fb6dd
[Solution] 3.21 Add another type of trees
fdardenne Oct 3, 2023
3cca348
[Solution] 4.1 Make a hello world view
fdardenne Sep 27, 2022
b1d6f57
[Solution] 4.2 Use the Layout component
fdardenne Sep 27, 2022
9dcdbce
[Solution] 4.3 Parse the arch
fdardenne Sep 27, 2022
a58611d
[Solution] 4.4 Load some data
fdardenne Sep 27, 2022
67caf39
[Solution] 4.5 Solve the concurrency problem
fdardenne Oct 18, 2023
77f9ad0
[Solution] 4.6 Reorganize code
fdardenne Sep 28, 2022
3016eee
[Solution] 4.7 Make the view extensible
fdardenne Oct 13, 2023
efe62c0
[Solution] 4.8 Display images
fdardenne Sep 28, 2022
ac465ad
[Solution] 4.9 Switch to form view on click
fdardenne Sep 29, 2022
d3e9035
[Solution] 4.10 Add an optional tooltip
fdardenne Sep 29, 2022
d85d203
[Solution] 4.11 Add pagination
fdardenne Sep 29, 2022
90d7ad8
[Solution] 4.12 Validating views
fdardenne Sep 30, 2022
d5d4d74
[Solution] 4.13 Upload button on images
fdardenne Oct 16, 2023
dba2e62
[Solution] 4.14 Advanced tooltip template
fdardenne Oct 18, 2023
651d92a
[Solution] 5.1: Create a new kanban view
fdardenne Oct 23, 2023
a2e866e
[Solution] 5.2: Create a CustomerList component
fdardenne Oct 23, 2023
76f4b4b
[Solution] 5.3: Load and display data
fdardenne Oct 26, 2023
9085cf3
[Solution] 5.4: Update the main kanban view
fdardenne Oct 27, 2023
3c61c54
[Solution] 5.5: Only display customers which have an active order
fdardenne Oct 30, 2023
0b949e5
[Solution] 5.6. Add a search bar to the customer list
fdardenne Oct 30, 2023
f2f8d99
[Solution] 5.7: Refactor the code to use t-model
fdardenne Oct 30, 2023
5e9d4bd
[Solution] 5.8: Paginate customers!
fdardenne Oct 30, 2023
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
1 change: 1 addition & 0 deletions awesome_clicker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
29 changes: 29 additions & 0 deletions awesome_clicker/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
{
'name': "Awesome Clicker",

'summary': """
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
""",

'description': """
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
""",

'author': "Odoo",
'website': "https://www.odoo.com/",
'category': 'Tutorials/AwesomeClicker',
'version': '0.1',
'application': True,
'installable': True,
'depends': ['base', 'web'],

'data': [],
'assets': {
'web.assets_backend': [
'awesome_clicker/static/src/**/*',
],

},
'license': 'AGPL-3'
}
26 changes: 26 additions & 0 deletions awesome_clicker/static/src/click_rewards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @odoo-module */

export const rewards = [
{
description: "Get 1 click bot",
apply(clicker) {
clicker.increment(1);
},
maxLevel: 3,
},
{
description: "Get 10 click bot",
apply(clicker) {
clicker.increment(10);
},
minLevel: 3,
maxLevel: 4,
},
{
description: "Increase bot power!",
apply(clicker) {
clicker.multipler += 1;
},
minLevel: 3,
},
];
8 changes: 8 additions & 0 deletions awesome_clicker/static/src/clicker_hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @odoo-module */

import { useService } from "@web/core/utils/hooks";
import { useState } from "@odoo/owl";

export function useClicker() {
return useState(useService("awesome_clicker.clicker"));
}
31 changes: 31 additions & 0 deletions awesome_clicker/static/src/clicker_migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** @odoo-module */

export const CURRENT_VERSION = 2.0;
export const migrations = [
{
fromVersion: 1.0,
toVersion: 2.0,
apply: (state) => {
state.trees.peachTree = {
price: 1500000,
level: 4,
produce: "peach",
purchased: 0,
};
state.fruits.peach = 0;
},
},
];

export function migrate(localState) {
if (localState?.version < CURRENT_VERSION) {
for (const migration of migrations) {
if (localState.version === migration.fromVersion) {
migration.apply(localState);
localState.version = migration.toVersion
}
}
localState.version = CURRENT_VERSION;
}
return localState;
}
149 changes: 149 additions & 0 deletions awesome_clicker/static/src/clicker_model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/** @odoo-module */

import { Reactive } from "@web/core/utils/reactive";
import { EventBus } from "@odoo/owl";
import { rewards } from "./click_rewards";
import { choose } from "./utils";
import { CURRENT_VERSION } from "./clicker_migration";

export class ClickerModel extends Reactive {
constructor() {
super();
this.version = CURRENT_VERSION;
this.clicks = 0;
this.level = 0;
this.bus = new EventBus();
this.bots = {
clickbot: {
price: 1000,
level: 1,
increment: 10,
purchased: 0,
},
bigbot: {
price: 5000,
level: 2,
increment: 100,
purchased: 0,
}
}
this.trees = {
pearTree: {
price: 1000000,
level: 4,
produce: "pear",
purchased: 0,
},
cherryTree: {
price: 1000000,
level: 4,
produce: "cherry",
purchased: 0,
},
peachTree: {
price: 1500000,
level: 4,
produce: "peach",
purchased: 0,
},
}
this.fruits = {
pear: 0,
cherry: 0,
peach: 0,
},
this.multiplier = 1


document.addEventListener("click", () => this.increment(1), true);
setInterval(() => {
for (const bot in this.bots) {
this.clicks += this.bots[bot].increment * this.bots[bot].purchased * this.multiplier;
}
}, 10000);

setInterval(() => {
for (const tree in this.trees) {
this.fruits[this.trees[tree].produce] += this.trees[tree].purchased;
}
}, 30000);
}

buyMultiplier() {
if (this.clicks < 50000) {
return false;
}
this.clicks -= 50000;
this.multiplier++;
}

increment(inc) {
this.clicks += inc;
if (
this.milestones[this.level] &&
this.clicks >= this.milestones[this.level].clicks
) {
this.bus.trigger("MILESTONE", this.milestones[this.level]);
this.level += 1;
}
}

buyBot(name) {
if (!Object.keys(this.bots).includes(name)) {
throw new Error(`Invalid bot name ${name}`);
}
if (this.clicks < this.bots[name].price) {
Comment thread
fdardenne marked this conversation as resolved.
return false;
}

this.clicks -= this.bots[name].price;
this.bots[name].purchased += 1;
}

giveReward() {
const availableReward = [];
for (const reward of rewards) {
if (reward.minLevel <= this.level || !reward.minLevel) {
if (reward.maxLevel >= this.level || !reward.maxLevel) {
availableReward.push(reward);
}
}
}
const reward = choose(availableReward);
this.bus.trigger("REWARD", reward);
return choose(availableReward);
}

buyTree(name) {
if (!Object.keys(this.trees).includes(name)) {
throw new Error(`Invalid tree name ${name}`);
}
if (this.clicks < this.trees[name].price) {
Comment thread
fdardenne marked this conversation as resolved.
return false;
}
this.clicks -= this.trees[name].price;
this.trees[name].purchased += 1;
}

toJSON() {
const json = Object.assign({}, this);
delete json["bus"];
return json;

}

static fromJSON(json) {
const clicker = new ClickerModel();
const clickerInstance = Object.assign(clicker, json);
return clickerInstance;
}

get milestones() {
return [
{ clicks: 1000, unlock: "clickBot" },
{ clicks: 5000, unlock: "bigBot" },
{ clicks: 100000, unlock: "power multiplier" },
{ clicks: 1000000, unlock: "pear tree & cherry tree" },
];
}
}
29 changes: 29 additions & 0 deletions awesome_clicker/static/src/clicker_provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** @odoo-module **/

import { registry } from "@web/core/registry";

const commandProviderRegistry = registry.category("command_provider");

commandProviderRegistry.add("clicker", {
provide: (env, options) => {
return [
{
name: "Buy 1 click bot",
action() {
env.services["awesome_clicker.clicker_service"].buyBot("clickbot");
},
},
{
name: "Open Clicker Game",
action() {
env.services.action.doAction({
type: "ir.actions.client",
tag: "awesome_clicker.client_action",
target: "new",
name: "Clicker Game",
});
},
}
]
},
});
55 changes: 55 additions & 0 deletions awesome_clicker/static/src/clicker_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/** @odoo-module */

import { registry } from "@web/core/registry";
import { ClickerModel } from "./clicker_model";
import { browser } from "@web/core/browser/browser";
import { migrate } from "./clicker_migration";

const clickerService = {
dependencies: ["action", "effect", "notification"],
start(env, services) {
const localState = migrate(JSON.parse(browser.localStorage.getItem("clickerState")));
const clickerModel = localState ? ClickerModel.fromJSON(localState): new ClickerModel();

Comment thread
fdardenne marked this conversation as resolved.
setInterval(() => {
browser.localStorage.setItem("clickerState", JSON.stringify(clickerModel))
}, 10000);

const bus = clickerModel.bus
bus.addEventListener("MILESTONE", (ev) => {
services.effect.add({
message: `Milestone reached! You can now buy ${ev.detail.unlock}`,
type: "rainbow_man",
});
});

bus.addEventListener("REWARD", (ev) => {
const reward = ev.detail;
const closeNotification = services.notification.add(
`Congrats you won a reward: "${reward.description}"`,
{
type: "success",
sticky: true,
buttons: [
{
name: "Collect",
onClick: () => {
reward.apply(clickerModel);
closeNotification();
services.action.doAction({
type: "ir.actions.client",
tag: "awesome_clicker.client_action",
target: "new",
name: "Clicker Game"
});
},
},
],
}
);
})
return clickerModel;
},
};

registry.category("services").add("awesome_clicker.clicker", clickerService);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/** @odoo-module */

import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useClicker } from "../clicker_hook";
import { ClickerValue } from "../clicker_value/clicker_value";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";

export class ClickerSystray extends Component {
static template = "awesome_clicker.ClickerSystray";
static components = { ClickerValue, Dropdown, DropdownItem };

setup() {
this.action = useService("action");
this.clicker = useClicker();
}

openClientAction() {
this.action.doAction({
type: "ir.actions.client",
tag: "awesome_clicker.client_action",
target: "new",
name: "Clicker Game"
});
}

get numberTrees() {
let sum = 0;
for (const tree in this.clicker.trees) {
sum += this.clicker.trees[tree].purchased;
}
return sum;
}

get numberFruits() {
let sum = 0;
for (const fruit in this.clicker.fruits) {
sum += this.clicker.fruits[fruit];
}
return sum;
}
}

export const systrayItem = {
Component: ClickerSystray,
};

registry.category("systray").add("awesome_clicker.ClickerSystray", systrayItem, { sequence: 1000 });
Loading