Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track Schema Migrations #144

Open
wants to merge 37 commits into
base: prod
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a7fccdb
Trigger minor diff
rtshkmr Oct 9, 2024
1f67b7a
Delete marks from read mode (#117)
rtshkmr Oct 12, 2024
0b97768
[BUGFIX] Explicitly pipe apply_action, not helm
Oct 14, 2024
18264a2
Edit marks in read mode (#118)
rtshkmr Oct 17, 2024
528cddf
Add a generic modal wrapper (#120)
rtshkmr Oct 17, 2024
4a696a0
Sheaf CRUD: Add UI skeleton, data plumbing for creating sheafs (#121)
rtshkmr Oct 26, 2024
48042ea
Add helpers to generate a family of sheafs for testing disc mode (#122)
rtshkmr Oct 26, 2024
91864de
Add data, ui lattices and ui primitives for the discuss context (#124)
rtshkmr Oct 26, 2024
ad6eb97
Refactor: read mode keeps sheaf state only (#125)
rtshkmr Oct 26, 2024
8c6a5f4
Read mode: reply_to relies on draft sheaf's parent (#127)
rtshkmr Oct 26, 2024
6facc00
the holy brace (#130)
ks0m1c Oct 27, 2024
0ddd526
Discuss: init drafting and reply_to contexts (#128)
rtshkmr Oct 27, 2024
bdfef0e
bind, share and jump around (#132)
ks0m1c Nov 11, 2024
b7cc96b
Disciple Panels (#137)
ks0m1c Dec 8, 2024
0f7e185
Version Bumping Deployment
Feb 15, 2025
a9e2a3a
Cleanup for Vel Marral
Feb 16, 2025
28b533d
MediaBridge Handshake Refactored
Feb 18, 2025
4ef88cf
MediaBridge Behaviour on verse update
Feb 19, 2025
933b3f0
smaller play button
Feb 24, 2025
68c8485
Sivaratri Night Data Groundwork
Feb 24, 2025
037c938
Fix type warnings
rtshkmr Feb 24, 2025
05a1762
Tracklist & Bhaj Schemas
Feb 25, 2025
5ad10d5
Bhaj Context for Tracklist
Feb 25, 2025
d6bdd90
Ruleset for Tamil
Feb 25, 2025
f7d1c29
Create Tracks Relationship
Feb 25, 2025
158307b
Merge branch 'ops/sivaratri' into ft/tracklist
rtshkmr Feb 25, 2025
414043a
Data Wrangling Patches
Feb 25, 2025
25f213b
Routing for Tracklists and Tracks
Feb 25, 2025
87f2a5f
emphasis decoupling
Feb 25, 2025
e95ee65
corpus migration for sivarathri night
Feb 26, 2025
8bf1d96
Main Tracks View
Mar 1, 2025
2256fad
MediaBridge & Keybind Groundwork
Mar 1, 2025
e0c999f
datawrangling for event
Mar 1, 2025
3bd2441
apply tracklist action to initiate playback
Mar 1, 2025
33593f9
emphasiseeee
Mar 1, 2025
60dd7f3
keybinded events
Mar 1, 2025
6120717
final sizing touchups for event
Mar 2, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/prod.yml
Original file line number Diff line number Diff line change
@@ -24,8 +24,8 @@ jobs:
--health-retries 5
strategy:
matrix:
otp: ['26.2']
elixir: ['1.16.3']
otp: ['27.2']
elixir: ['1.17.3']
steps:
- name: Checkout code
uses: actions/checkout@v4 # Pin to a specific version for stability
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -11,9 +11,9 @@
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.15.7-erlang-26.1.1-debian-bullseye-20230612-slim
#
ARG ELIXIR_VERSION=1.15.7
ARG OTP_VERSION=26.1.1
ARG DEBIAN_VERSION=bullseye-20230612-slim
ARG ELIXIR_VERSION=1.17.3
ARG OTP_VERSION=27.2.2
ARG DEBIAN_VERSION=bullseye-20250203-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
1 change: 0 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ Wherever there be anything you dost not comprehend, cease to continue writing
-- Vyasa, Adi Parva - Mahabharatam
#+END_QUOTE


* What is the _*Vyasa Project*_?
=TODO=

4 changes: 2 additions & 2 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@
--white-alabaster: #F3EFE3;
--linen: #FAF1E6;

--aerospace-orange: #FD4F00;
--deep-saffron: #ff9933;
--coral-orange: #FF8349;
--atomic-tangerine: #FF9B6D;

@@ -77,7 +77,7 @@
/* This file is for your main application CSS */

.emphasized-verse {
@apply bg-brandAccentLight border-b-0 border-l-8 border-black p-4 pl-8 rounded-sm;
@apply bg-orange-400/30 border-b-0 border-l-8 border-red-600 p-4 pl-8 rounded-sm;
}

@font-face {
72 changes: 70 additions & 2 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -36,6 +36,16 @@ let liveSocket = new LiveSocket("/live", Socket, {

session: fetchSession(),
},
metadata: {
keydown: (event, element) => {
return {
key: event.key,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
};
},
},

hooks: Hooks,
});

@@ -73,11 +83,69 @@ function genAnonId(length = 18) {
}


let lastEmphasizedElement = null;

function emphasizeVerseElement(selectorId, className) {
console.log("LESGOO EMPHASISE", selectorId);

// remove emphasis from prev
if (lastEmphasizedElement && lastEmphasizedElement.classList.contains(className)) {
lastEmphasizedElement.classList.remove(className);
lastEmphasizedElement = null;
}

const escapedId = CSS.escape(selectorId);
const element = document.querySelector(`[emph_verse_id="${escapedId}"]`);

if (element) {

if (!element.classList.contains(className)) {
element.classList.add(className);
}


element.scrollIntoView({ behavior: 'smooth', block: 'center' });


if (element.tabIndex < 0) {

element.tabIndex = -1;
}
element.focus({ preventScroll: true });


lastEmphasizedElement = element;
} else {
console.warn(`Element with verse_id "${selectorId}" not found`);

// Fallback: try searching by verse_id failed
const nodeElement = document.querySelector(`[verse_id="${escapedId}"]`);
if (nodeElement) {
console.log("Found element by node_id instead");
nodeElement.classList.add(className);
nodeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });

if (nodeElement.tabIndex < 0) {
nodeElement.tabIndex = -1;
}
nodeElement.focus({ preventScroll: true });

lastEmphasizedElement = nodeElement;
}
}
}


window.addEventListener("phx:verseEmphasis", (e) => {
const { verseId, className } = e.detail;

emphasizeVerseElement(verseId, className)
});


// Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
topbar.config({ barColors: { 0: "#f6d4ad" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(200));
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());

// Stream our server logs directly to our browser’s console
46 changes: 32 additions & 14 deletions assets/js/hooks/hoverune.js
Original file line number Diff line number Diff line change
@@ -35,16 +35,18 @@ function floatHoveRune({ clientX, clientY }) {
top: `${y}px`,
});
});

// computePosition(virtualEl, hoverune, {placement: 'top-end', middleware: [inline(getSelectRect.x, getSelectRect.y), offset(5)]}).then(({x, y}) => {
// hoverune.classList.remove("hidden")
// Object.assign(hoverune.style, {
// left: `${getSelectRect.x}px`,
// top: `${y}px`,
// });
// })
}

const findMatchingSpan = ({ node_id, field }) => {
// Early return if missing either criteria
if (!node_id || !field) return null;

// Find first span with both exact matches
return document.querySelector(
`span[node_id="${node_id}"][field="${field}"]`
);
};

const findHook = (el) => findParent(el, "phx-hook", "HoveRune");
const findMarginote = (el) => findParent(el, "phx-hook", "MargiNote");
const findNode = (el) => el && el.getAttribute("node");
@@ -62,36 +64,52 @@ export default HoveRune = {
console.log("CHECK HOVERUNE", {
dset: this.el.dataset,
});
const targetEvents = ["pointerdown", "pointerup"];

this.handleEvent("bind::jump", (bind) => {
console.warn(bind)
targetNode = findMatchingSpan(bind)
if (targetNode) {
targetNode.focus();
targetNode.scrollIntoView({
behavior: "smooth",
block: "center",
});
}

});
const targetEvents = ["pointerdown", "pointerup"];
targetEvents.forEach((e) =>
window.addEventListener(e, ({ target }) => {
var selection = window.getSelection();
if (!selection || selection.rangeCount <= 0) {
return;
}

var getSelectRect = selection.getRangeAt(0).getBoundingClientRect();
var range = selection.getRangeAt(0);
var getSelectRect = range.getBoundingClientRect();
const getSelectText = selection.toString();
//const validElem = findHook(target)
// const isMarginote = findMarginote(target)
const isNode = findNode(target);

console.log("binding selected here!")
console.log(selection)

if (isNode) {
binding = forgeBinding(target, [
"node",
"node_id",
"text",
"field",
"verse_id",
]);
binding["selection"] = getSelectText;

console.log("CHECK HOVERUNE", {
eventTarget: this.eventTarget,
target: `#${this.eventTarget}`,
payload: { binding: binding },
});
this.pushEventTo(`#${this.eventTarget}`, "bindHoveRune", {
this.pushEvent("bind::to", {
binding: binding,
target: this.eventTarget
});

console.log(binding);
4 changes: 4 additions & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ import HoveRune from "./hoverune.js";
import Scrolling from "./scrolling.js";
import ButtonClickRelayer from "./button_click_relayer.js";
import SessionBox from "./session_box.js";
import TextareaAutoResize from "./textarea_auto_resize.js";
import PseudoForm from "./pseudo_form.js";

let Hooks = {
ShareQuoteButton,
@@ -26,6 +28,8 @@ let Hooks = {
Scrolling,
ButtonClickRelayer,
SessionBox,
TextareaAutoResize,
PseudoForm,
};

export default Hooks;
103 changes: 103 additions & 0 deletions assets/js/hooks/pseudo_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* {PseudoForm}
*
* This hook facilitates the capture of input values from "form elements" without resorting to
* tradition form-submission mechanisms. This approach is useful to avoid the nested form problem -- wherein
* nested forms lead to unintended behaviour because a submission event in a child form will get bubbled out (propagated)
* to the parent, as per HTML-DOM spec and will cause the parent's submit to also be triggerred. This would mean that
* without the user desiring it so, there will be a form submission. While there are ways to hack this, for example by
* preventing the bubble propagation from happening, those solutions are not long-term, which brings us to
* consider this hook as an alternative approach. By using this hook, we can effectively manage input values while keeping
* components stateless and avoiding the complexities associated with nested forms.

* **Why Nested Forms Are an Antipattern**: [perplexity-generated section]
* - **Event Propagation Issues**: Nested forms can cause submit events to bubble up, leading to unintended submissions of parent forms.
* - **Complexity**: Managing state and events across nested forms increases the complexity of your application, making it harder to maintain.
* - **Accessibility Concerns**: Screen readers and assistive technologies may struggle with nested forms, potentially leading to a poor user experience.
* - **HTML Specification**: The HTML specification does not support nested forms, which can lead to inconsistent behavior across different browsers.
*
* **Value of Stateless Function Components**: [perplexity-generated section]
* - Using function components that do not rely on form state allows for greater composability and flexibility.
* - It enables nesting of components without the constraints imposed by traditional form handling, allowing for cleaner and more maintainable code.
* - This approach promotes reusability and modularity within your codebase, making it easier to build complex UIs without the overhead of managing form state.
*
* Overall Mechanism for this hook:
* - The hook listens for a specified event on the element it is attached to.
* - When the event is triggered, it captures the value of a designated input element.
* - It then pushes an event to the LiveView server with the captured input value and any additional payload.
*
* Expected Dataset Parameters:
* - `data-event-to-capture`: A string representing the type of event to listen for (e.g., "click").
* This allows flexibility in determining which user interaction will trigger the value capture.
*
* - `data-target-selector`: A CSS selector string that identifies the input element from which to capture
* the value. This makes the hook generic and reusable for different types of inputs (e.g., textareas,
* text inputs).
*
* - `data-event-name`: A string representing the name of the event to be pushed to the LiveView server.
* If not provided, it defaults to "submitPseudoForm".
*
* - `data-event-target`: A string representing the target for the event push. This should match the
* target LiveView or component that will handle the event on the server side.
*
* - `data-event-payload`: A JSON string representing any additional data you want to send along with
* the captured input value. This allows for more complex interactions without needing to modify
* component state externally. This is actually a static part of the eventual payload that the event needs to have, we shall merge this with the input that we will be reading.
*
* Example Usage:
* <button
* phx-hook="PseudoForm"
* data-event-to-capture="click"
* data-target-selector="#input-id"
* data-event-name="mark::editMarkContent"
* data-event-target="targetLiveView"
* data-event-payload='{"additional_key": "additional_value"}'
* >
* Submit
* </button>
*
* NOTE: this currently only handles single-input fields. This hook may be extended to
* handle a group of input fields following a similar approach; otherwise, we should fallback to
* typical LiveView idioms that deal with how to handle forms.
*/

PseudoForm = {
mounted() {
const eventToCapture = this.el.getAttribute("data-event-to-capture");
this.el.addEventListener(eventToCapture, (_event) => {
const targetSelector = this.el.getAttribute("data-target-selector");
const eventName =
this.el.getAttribute("data-event-name") || "submitPseudoForm";
const eventPayload = JSON.parse(
this.el.getAttribute("data-event-payload") || "{}",
);
const eventTarget = this.el.getAttribute("data-event-target");
const inputElement = document.querySelector(targetSelector);

if (inputElement) {
const value = inputElement.value; // Reads the current value of the input
const finalPayload = {
...eventPayload,
input: value.trim(),
};
console.info("Using a pseudoform", {
eventName,
eventTarget,
eventPayload,
value,
finalPayload,
});
this.pushEventTo(eventTarget, eventName, finalPayload);
} else {
console.warn("Desired input element not found, params:", {
targetSelector,
eventName,
eventTarget,
eventPayload,
});
}
});
},
};

export default PseudoForm;
12 changes: 12 additions & 0 deletions assets/js/hooks/session_box.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
SessionBox = {
mounted() {
this.handleEvent("initSession", (sess) => this.initSession(sess));

this.handleEvent("session::share", (bind) => {
if ("share" in navigator) {
// uses webshare api:
window.shareUrl(bind.url);
} else if ("clipboard" in navigator) {
navigator.clipboard.writeText(bind.url);
} else {
alert("Here is the url to share: #{bind.url}");
}

});
},

initSession(sess) {
14 changes: 14 additions & 0 deletions assets/js/hooks/textarea_auto_resize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* This hook, if injected to a textarea, will automatically resize the
* textarea height (bound by max height, if already defined).
* */

export default TextareaAutoResize = {
mounted() {
this.handleInput();
},
handleInput() {
this.el.style.height = "auto"; // Resets height to auto to shrink if needed
this.el.style.height = `${this.el.scrollHeight}px`; // Sets height based on scrollHeight
},
};
Loading