···3use log::info;
4use serde_json::Value;
5use std::rc::Rc;
06use wasm_bindgen_futures::spawn_local;
7use yew::prelude::*;
8use yew_hooks::use_effect_once;
···12// Import the Store trait
13use atrium_common::store::Store;
1415+mod app_routes;
16mod atrium_stores;
17+mod auth_store;
18+mod components;
19mod idb;
20mod oauth_client;
21+mod pages;
2223// Add pages module
24+// pub mod pages; // Remove this duplicate
2526// Import page components
27use crate::pages::callback::CallbackPage;
···35use atrium_api::types::string::Did;
3637const API_ROOT: &str = "";
38+39+// Define CounterState struct for local use in app_demo
40+#[derive(Clone, Debug, PartialEq, Default, serde::Deserialize, serde::Serialize)]
41+pub struct CounterState {
42+ // Made pub for potential use in fetch_counter etc. if they move
43+ pub value: i32,
44+}
4546// Define our application state
47#[derive(Clone, Debug, PartialEq, Default)]
+193-6
rust_demo_app/app_demo/src/pages/home.rs
···1use crate::{
2 fetch_counter,
3- increment_counter,
4 reset_counter,
5 AppState, // Import reducer state
6 AuthStore,
7};
00000000000008use yew::prelude::*;
9-use yewdux::prelude::*;
1011#[function_component(HomePage)]
12pub fn home_page() -> Html {
···14 let state = use_reducer(AppState::default);
15 let (auth_store, _) = use_store::<AuthStore>(); // Get auth state to check if logged in
160000000000000000000000000000000000000000000000000000000000000000000000000000017 // Effect to load counter on mount
18 {
19 let state = state.clone();
···24 }
2526 let on_increment = {
27- let state = state.clone();
000028 Callback::from(move |_| {
29- increment_counter(state.clone());
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000030 })
31 };
32···55 } else if let Some(counter_data) = &state.counter {
56 <p class="text-3xl font-bold">{ counter_data.value }</p>
57 <div class="mt-4 space-x-2">
58- <button onclick={on_increment} class="btn btn-primary">{ "Increment" }</button>
59- <button onclick={on_reset} class="btn btn-secondary">{ "Reset" }</button>
00000060 </div>
61 } else {
62 <p>{ "Counter state not available." }</p>
···1use crate::{
2 fetch_counter,
3+ oauth_client::get_oauth_client,
4 reset_counter,
5 AppState, // Import reducer state
6 AuthStore,
7};
8+use atrium_api::agent::Agent; // Use Agent for XRPC calls
9+use atrium_api::com::atproto::repo::{
10+ get_record::ParametersData as GetRecordParametersData,
11+ put_record::InputData as PutRecordInputData,
12+}; // Import specific data structs
13+use atrium_api::types::string::{/* AtIdentifier, // Removed */ Datetime, Nsid, RecordKey};
14+use atrium_api::types::{DataModel, /* Object, // Removed */ Unknown};
15+use ipld_core::serde::to_ipld;
16+use serde_json; // Added for record serialization
17+use std::convert::TryFrom; // Added for try_from conversions
18+use types_demo::dev::alternatebuild::rust_demo::count::{
19+ /* Record as CountRecord, // Removed */ RecordData as CountRecordData,
20+};
21use yew::prelude::*;
22+use yewdux::prelude::*; // Added for serde_ipld_dagcbor::to_ipld
2324#[function_component(HomePage)]
25pub fn home_page() -> Html {
···27 let state = use_reducer(AppState::default);
28 let (auth_store, _) = use_store::<AuthStore>(); // Get auth state to check if logged in
2930+ // ATProto specific state
31+ let has_counted_record = use_state(|| false);
32+ let is_checking_record = use_state(|| true);
33+ let increment_button_disabled = use_state(|| true);
34+ let increment_button_text = use_state(|| "Increment".to_string());
35+36+ // Effect to check for existing ATProto record
37+ {
38+ let auth_store_for_effect = auth_store.clone();
39+ let has_counted_record = has_counted_record.clone();
40+ let is_checking_record = is_checking_record.clone();
41+ let increment_button_disabled = increment_button_disabled.clone();
42+ let increment_button_text = increment_button_text.clone();
43+44+ use_effect_with(auth_store_for_effect.did.clone(), move |did_option| {
45+ let did_option = did_option.clone();
46+ let has_counted_record = has_counted_record.clone();
47+ let is_checking_record = is_checking_record.clone();
48+ let increment_button_disabled = increment_button_disabled.clone();
49+ let increment_button_text = increment_button_text.clone();
50+51+ is_checking_record.set(true);
52+53+ if let Some(did) = did_option.clone() {
54+ wasm_bindgen_futures::spawn_local(async move {
55+ let oauth_client = get_oauth_client().await;
56+ match oauth_client.restore(&did).await {
57+ Ok(session) => {
58+ let agent = Agent::new(session);
59+ let collection =
60+ Nsid::new(String::from("dev.alternatebuild.rust_demo.count"))
61+ .expect("Static NSID failed to parse");
62+ let rkey = RecordKey::new(String::from("self"))
63+ .expect("Static RecordKey failed to parse");
64+ let params = GetRecordParametersData {
65+ repo: did.into(),
66+ collection,
67+ rkey,
68+ cid: None,
69+ }
70+ .into();
71+72+ match agent.api.com.atproto.repo.get_record(params).await {
73+ Ok(_) => {
74+ has_counted_record.set(true);
75+ increment_button_text.set("Already Counted".to_string());
76+ increment_button_disabled.set(true);
77+ }
78+ Err(e) => {
79+ // Check if error is RecordNotFound
80+ // (Need to inspect the specific error type `e` from `get_record` result)
81+ // For now, assume any error means no record or inaccessible
82+ has_counted_record.set(false);
83+ increment_button_text.set("Increment".to_string());
84+ increment_button_disabled.set(false);
85+ log::debug!("get_record failed (likely not found): {:?}", e);
86+ }
87+ }
88+ }
89+ Err(_e) => {
90+ has_counted_record.set(false);
91+ increment_button_text.set("Session error. Try login.".to_string());
92+ increment_button_disabled.set(true);
93+ }
94+ }
95+ is_checking_record.set(false);
96+ });
97+ } else {
98+ has_counted_record.set(false);
99+ increment_button_disabled.set(true);
100+ increment_button_text.set("Login to Count".to_string());
101+ is_checking_record.set(false);
102+ }
103+ || ()
104+ });
105+ }
106+107 // Effect to load counter on mount
108 {
109 let state = state.clone();
···114 }
115116 let on_increment = {
117+ let auth_store_for_increment = auth_store.clone();
118+ let has_counted_record_for_increment = has_counted_record.clone();
119+ let increment_button_disabled_for_increment = increment_button_disabled.clone();
120+ let increment_button_text_for_increment = increment_button_text.clone();
121+122 Callback::from(move |_| {
123+ let auth_store = auth_store_for_increment.clone();
124+ let has_counted_record = has_counted_record_for_increment.clone();
125+ let increment_button_disabled = increment_button_disabled_for_increment.clone();
126+ let increment_button_text = increment_button_text_for_increment.clone();
127+128+ if !*has_counted_record && auth_store.did.is_some() {
129+ let did_for_put = auth_store.did.clone().unwrap();
130+ wasm_bindgen_futures::spawn_local(async move {
131+ let oauth_client = get_oauth_client().await;
132+ match oauth_client.restore(&did_for_put).await {
133+ Ok(session) => {
134+ let agent = Agent::new(session);
135+ let record_body = CountRecordData {
136+ created_at: Datetime::now(),
137+ };
138+ // Serialize record_body to Unknown via Ipld & DataModel
139+ let record_unknown = match serde_json::to_value(record_body) {
140+ Ok(value) => match to_ipld(&value) {
141+ Ok(ipld) => match DataModel::try_from(ipld) {
142+ Ok(data_model) => Unknown::Other(data_model), // Construct Unknown
143+ Err(e) => {
144+ log::error!(
145+ "Failed to convert Ipld to DataModel: {:?}",
146+ e
147+ );
148+ increment_button_text
149+ .set("DataModel Error".to_string());
150+ increment_button_disabled.set(true);
151+ return;
152+ }
153+ },
154+ Err(e) => {
155+ log::error!(
156+ "Failed to convert serde_json::Value to Ipld: {:?}",
157+ e
158+ );
159+ increment_button_text
160+ .set("Ipld Conversion Error".to_string());
161+ increment_button_disabled.set(true);
162+ return;
163+ }
164+ },
165+ Err(e) => {
166+ log::error!("Failed to serialize record body: {:?}", e);
167+ increment_button_text.set("Serialization Error".to_string());
168+ increment_button_disabled.set(true);
169+ return;
170+ }
171+ };
172+173+ let collection =
174+ Nsid::new(String::from("dev.alternatebuild.rust_demo.count"))
175+ .expect("Static NSID failed to parse");
176+ let rkey = RecordKey::new(String::from("self"))
177+ .expect("Static RecordKey failed to parse");
178+ let input = PutRecordInputData {
179+ repo: did_for_put.into(),
180+ collection,
181+ rkey,
182+ validate: Some(true),
183+ record: record_unknown,
184+ swap_commit: None,
185+ swap_record: None,
186+ }
187+ .into();
188+189+ match agent.api.com.atproto.repo.put_record(input).await {
190+ // Pass input struct
191+ Ok(_) => {
192+ has_counted_record.set(true);
193+ increment_button_disabled.set(true);
194+ increment_button_text.set("Counted!".to_string());
195+ }
196+ Err(_e) => {
197+ increment_button_text
198+ .set("Failed to count. Retry?".to_string());
199+ increment_button_disabled.set(false);
200+ log::error!("put_record failed: {:?}", _e);
201+ }
202+ }
203+ }
204+ Err(_e) => {
205+ increment_button_text.set("Session error. Try login.".to_string());
206+ increment_button_disabled.set(true);
207+ }
208+ }
209+ });
210+ }
211 })
212 };
213···236 } else if let Some(counter_data) = &state.counter {
237 <p class="text-3xl font-bold">{ counter_data.value }</p>
238 <div class="mt-4 space-x-2">
239+ if *is_checking_record {
240+ <p>{ "Checking your ATProto record..." }</p>
241+ } else {
242+ <button onclick={on_increment} disabled={*increment_button_disabled} class="btn btn-primary">
243+ { &*increment_button_text }
244+ </button>
245+ }
246+ <button onclick={on_reset} class="btn btn-secondary">{ "Reset Shared Counter" }</button>
247 </div>
248 } else {
249 <p>{ "Counter state not available." }</p>
+27-3
rust_demo_app/justfile
···9# Run both API and frontend servers
10dev:
11 #!/usr/bin/env sh
12- trap 'kill $(jobs -p)' EXIT
13- just api &
14- just app 000000000000000000000000
···9# Run both API and frontend servers
10dev:
11 #!/usr/bin/env sh
12+ # Start API in background and get its PID
13+ just api &
14+ API_PID=$!
15+ echo "API server started in background (PID: $API_PID)"
16+17+ # Define cleanup function
18+ cleanup() {
19+ echo "Shutting down API server (PID: $API_PID)..."
20+ # Use kill -0 to check if process exists before trying to kill
21+ if kill -0 $API_PID 2>/dev/null; then
22+ kill $API_PID
23+ wait $API_PID 2>/dev/null # Wait briefly for it to exit
24+ else
25+ echo "API server (PID: $API_PID) not found."
26+ fi
27+ # Exit script cleanly
28+ exit 0
29+ }
30+31+ # Trap signals to run cleanup
32+ trap cleanup INT TERM EXIT
33+34+ # Start frontend in foreground (script will block here until app stops)
35+ just app
36+37+ # Fallback cleanup if app exits normally (trap EXIT should handle Ctrl+C)
38+ cleanup
+1
rust_demo_app/types_demo/Cargo.toml
···6[dependencies]
7schemars = { version = "0.8.22", features = ["derive"] }
8serde = { version = "1.0", features = ["derive"] }
0
···6[dependencies]
7schemars = { version = "0.8.22", features = ["derive"] }
8serde = { version = "1.0", features = ["derive"] }
9+atrium-api = { git = "https://github.com/atrium-rs/atrium" }
···1+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2+//!Definitions for the `dev` namespace.
3+pub mod alternatebuild;
···1+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2+//!Definitions for the `dev.alternatebuild` namespace.
3+4+pub mod rust_demo;
···1+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2+//!Definitions for the `dev.alternatebuild.rust_demo` namespace.
3+pub mod count;
4+#[derive(Debug)]
5+pub struct Count;
6+impl atrium_api::types::Collection for Count {
7+ const NSID: &'static str = "dev.alternatebuild.rust_demo.count";
8+ type Record = count::Record;
9+}