panda_cms 0.5.10 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/Rakefile +0 -1
- data/app/assets/builds/panda_cms.css +2415 -1
- data/app/assets/config/panda_cms_manifest.js +2 -0
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +3 -27
- data/app/builders/panda_cms/form_builder.rb +1 -1
- data/app/components/panda_cms/admin/button_component.rb +6 -3
- data/app/components/panda_cms/admin/flash_message_component.rb +1 -1
- data/app/components/panda_cms/admin/tag_component.rb +1 -1
- data/app/components/panda_cms/code_component.rb +60 -0
- data/app/components/panda_cms/page_menu_component.html.erb +6 -4
- data/app/components/panda_cms/page_menu_component.rb +21 -12
- data/app/components/panda_cms/rich_text_component.html.erb +6 -38
- data/app/components/panda_cms/rich_text_component.rb +24 -7
- data/app/components/panda_cms/text_component.rb +25 -22
- data/app/controllers/panda_cms/admin/dashboard_controller.rb +14 -6
- data/app/controllers/panda_cms/admin/menus_controller.rb +1 -54
- data/app/controllers/panda_cms/admin/pages_controller.rb +2 -1
- data/app/controllers/panda_cms/admin/sessions_controller.rb +13 -6
- data/app/controllers/panda_cms/application_controller.rb +1 -1
- data/app/controllers/panda_cms/pages_controller.rb +1 -1
- data/app/controllers/panda_cms/posts_controller.rb +1 -1
- data/app/helpers/panda_cms/application_helper.rb +2 -2
- data/app/javascript/panda_cms/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda_cms/@hotwired--turbo.js +160 -0
- data/app/javascript/panda_cms/@rails--actioncable--src.js +4 -0
- data/app/javascript/panda_cms/application_panda_cms.js +4 -0
- data/app/javascript/panda_cms/controllers/dashboard_controller.js +7 -0
- data/app/javascript/panda_cms/controllers/index.js +42 -0
- data/app/javascript/panda_cms/controllers/slug_controller.js +48 -0
- data/app/javascript/panda_cms/panda_cms_editable.js +248 -0
- data/app/javascript/panda_cms/tailwindcss-stimulus-components.js +4 -0
- data/app/lib/panda_cms/demo_site_generator.rb +1 -1
- data/app/lib/panda_cms/slug.rb +1 -1
- data/app/models/panda_cms/block.rb +2 -2
- data/app/models/panda_cms/page.rb +9 -3
- data/app/models/panda_cms/post.rb +1 -1
- data/app/models/panda_cms/template.rb +4 -2
- data/app/models/panda_cms/user.rb +9 -1
- data/app/views/panda_cms/admin/dashboard/show.html.erb +11 -9
- data/app/views/panda_cms/admin/forms/new.html.erb +6 -7
- data/app/views/panda_cms/admin/menus/index.html.erb +0 -2
- data/app/views/panda_cms/admin/pages/edit.html.erb +18 -16
- data/app/views/panda_cms/admin/pages/new.html.erb +6 -7
- data/app/views/panda_cms/admin/posts/_form.html.erb +4 -4
- data/app/views/panda_cms/admin/sessions/new.html.erb +1 -2
- data/app/views/panda_cms/admin/shared/_sidebar.html.erb +12 -16
- data/app/views/panda_cms/shared/_header.html.erb +14 -14
- data/app/views/panda_cms/shared/_importmap.html.erb +22 -0
- data/config/importmap.rb +11 -10
- data/config/initializers/panda_cms.rb +57 -55
- data/config/routes.rb +9 -9
- data/config/tailwind.config.js +1 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +6 -4
- data/lib/generators/panda_cms/install_generator.rb +3 -0
- data/lib/panda_cms/engine.rb +27 -22
- data/lib/panda_cms/version.rb +1 -1
- data/lib/panda_cms.rb +58 -10
- data/lib/tasks/panda_cms.rake +41 -57
- data/public/panda-cms-assets/rich_text_editor.css +568 -0
- metadata +216 -278
- data/app/javascript/base.js +0 -37
- data/app/javascript/controllers/menu_controller.js +0 -19
- data/app/javascript/controllers/text_controller.js +0 -78
- data/app/javascript/controllers/text_field_update_controller.js +0 -51
- data/app/javascript/vendor/stimulus-components-rails-nested-form.js +0 -2
- data/app/javascript/vendor/tailwindcss-stimulus-components.js +0 -2
- data/app/views/panda_cms/admin/menus/_form.html.erb +0 -21
- data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +0 -7
- data/app/views/panda_cms/admin/menus/edit.html.erb +0 -58
- data/app/views/panda_cms/admin/menus/new.html.erb +0 -5
- data/public/panda-cms-assets/javascripts/base.js +0 -37
- data/public/panda-cms-assets/javascripts/controllers/menu_controller.js +0 -19
- data/public/panda-cms-assets/javascripts/controllers/text_field_update_controller.js +0 -23
- data/public/panda-cms-assets/javascripts/embed/editable.js +0 -358
- data/public/panda-cms-assets/javascripts/embed/rich_text.css +0 -1294
- data/public/panda-cms-assets/javascripts/vendor/stimulus-components-rails-nested-form.js +0 -2
- data/public/panda-cms-assets/javascripts/vendor/stimulus-loading.js +0 -113
- data/public/panda-cms-assets/javascripts/vendor/tailwindcss-stimulus-components.js +0 -2
- /data/db/migrate/{20240804110225_add_status_to_panda_cms_pages.rb → 20240315125411_add_status_to_panda_cms_pages.rb} +0 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export class SlugController extends Controller {
|
4
|
+
static targets = [
|
5
|
+
"existing_root",
|
6
|
+
"input_select",
|
7
|
+
"input_text",
|
8
|
+
"output_text",
|
9
|
+
];
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
console.debug("[Panda CMS] Slug handler connected...");
|
13
|
+
}
|
14
|
+
|
15
|
+
generatePath() {
|
16
|
+
this.output_textTarget.value = "/" + this.createSlug(this.input_textTarget.value);
|
17
|
+
console.log("Have set the path to: " + this.output_textTarget.value);
|
18
|
+
}
|
19
|
+
|
20
|
+
setPrePath() {
|
21
|
+
this.parent_slugs = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/)[1];
|
22
|
+
this.output_textTarget.previousSibling.innerHTML = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, "");
|
23
|
+
console.log("Have set the pre-path to: " + this.output_textTarget.previousSibling.innerHTML);
|
24
|
+
}
|
25
|
+
|
26
|
+
// TODO: Invoke a library or helper which can be shared with the backend
|
27
|
+
// and check for uniqueness at the same time
|
28
|
+
createSlug(input) {
|
29
|
+
var str = input
|
30
|
+
.toLowerCase()
|
31
|
+
.trim()
|
32
|
+
.replace(/[^\w\s-]/g, "-")
|
33
|
+
.replace(/&/g, "and")
|
34
|
+
.replace(/[\s_-]+/g, "-")
|
35
|
+
.trim();
|
36
|
+
|
37
|
+
return this.trimStartEnd(str, "-");
|
38
|
+
}
|
39
|
+
|
40
|
+
trimStartEnd(str, ch) {
|
41
|
+
var start = 0;
|
42
|
+
var end = str.length;
|
43
|
+
|
44
|
+
while (start < end && str[start] === ch) ++start;
|
45
|
+
while (end > start && str[end - 1] === ch) --end;
|
46
|
+
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
47
|
+
}
|
48
|
+
}
|
@@ -0,0 +1,248 @@
|
|
1
|
+
/**
|
2
|
+
* Represents a controller for managing editable content within an iframe.
|
3
|
+
* @class
|
4
|
+
*/
|
5
|
+
class PandaCmsEditableController {
|
6
|
+
/**
|
7
|
+
* Represents the constructor for the Editable class.
|
8
|
+
* @param {HTMLIFrameElement} frame - The iFrame element to be used for editing.
|
9
|
+
*/
|
10
|
+
constructor(pageId, frame) {
|
11
|
+
this.pageId = pageId;
|
12
|
+
this.frame = frame;
|
13
|
+
this.frame.style.display = "none";
|
14
|
+
this.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
15
|
+
|
16
|
+
var pathNameArray = window.location.pathname.split("/");
|
17
|
+
this.adminPath = pathNameArray[1];
|
18
|
+
|
19
|
+
this.frame.addEventListener("load", () => {
|
20
|
+
this.frameDocument = this.frame.contentDocument || this.frame.contentWindow.document;
|
21
|
+
this.body = this.frameDocument.body;
|
22
|
+
this.head = this.frameDocument.head;
|
23
|
+
this.loadEvents();
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
/* iFrame & Main Methods */
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Load events for the editable iFrame
|
31
|
+
*/
|
32
|
+
loadEvents() {
|
33
|
+
console.debug("[Panda CMS] iFrame loaded...");
|
34
|
+
|
35
|
+
this.embedPlainTextEditors();
|
36
|
+
this.embedTrix();
|
37
|
+
}
|
38
|
+
|
39
|
+
setFrameVisible() {
|
40
|
+
console.debug("[Panda CMS] Setting iFrame to visible...");
|
41
|
+
this.frame.style.display = "";
|
42
|
+
}
|
43
|
+
|
44
|
+
addStylesheet(frameDocument, head, href) {
|
45
|
+
return new Promise(function (resolve, reject) {
|
46
|
+
let link = frameDocument.createElement("link");
|
47
|
+
link.rel = "stylesheet";
|
48
|
+
link.href = href;
|
49
|
+
link.media = "none";
|
50
|
+
head.append(link);
|
51
|
+
|
52
|
+
link.onload = () => {
|
53
|
+
if (link.media != "all") {
|
54
|
+
link.media = "all";
|
55
|
+
}
|
56
|
+
console.debug(`[Panda CMS] Stylesheet loaded: ${href}`);
|
57
|
+
resolve(link);
|
58
|
+
};
|
59
|
+
link.onerror = () =>
|
60
|
+
reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`));
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
64
|
+
loadScript(frameDocument, head, src) {
|
65
|
+
return new Promise(function (resolve, reject) {
|
66
|
+
let script = frameDocument.createElement("script");
|
67
|
+
script.src = src;
|
68
|
+
head.append(script);
|
69
|
+
|
70
|
+
script.onload = () => {
|
71
|
+
console.debug(`[Panda CMS] Script loaded: ${src}`);
|
72
|
+
resolve(script);
|
73
|
+
};
|
74
|
+
script.onerror = () =>
|
75
|
+
reject(new Error(`[Panda CMS] Script load error for ${src}`));
|
76
|
+
});
|
77
|
+
}
|
78
|
+
|
79
|
+
/* Plain Text Editor (inc. Code) Methods */
|
80
|
+
|
81
|
+
stylePlainTextEditor(element, status) {
|
82
|
+
console.debug(
|
83
|
+
`[Panda CMS] Styling editor ${element.id} as ${status}...`
|
84
|
+
);
|
85
|
+
|
86
|
+
if (status == "initial") {
|
87
|
+
element.style.border = "1px dashed #ccc";
|
88
|
+
element.style.outline = "none";
|
89
|
+
element.style.cursor = "pointer";
|
90
|
+
element.style.transition = "background 500ms linear";
|
91
|
+
element.style.backgroundColor = "inherit";
|
92
|
+
} else if (status == "success") {
|
93
|
+
element.style.backgroundColor = "#66bd6a50";
|
94
|
+
} else if (status == "error") {
|
95
|
+
element.style.backgroundColor = "#dc354550";
|
96
|
+
}
|
97
|
+
|
98
|
+
if (element.getAttribute("data-editable-kind") == "html") {
|
99
|
+
element.style.whiteSpace = "pre-wrap";
|
100
|
+
element.style.fontFamily = "monospace";
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
embedPlainTextEditors() {
|
105
|
+
var elements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]');
|
106
|
+
if (elements.length == 0) {
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
|
110
|
+
elements.forEach((element) => {
|
111
|
+
this.stylePlainTextEditor(element, "initial");
|
112
|
+
var currentElement = element;
|
113
|
+
|
114
|
+
// On save click, save this element
|
115
|
+
document.getElementById('saveEditableButton').addEventListener('click', () => {
|
116
|
+
this.bindPlainTextSaveHandler(currentElement);
|
117
|
+
});
|
118
|
+
|
119
|
+
console.debug("[Panda CMS] Attached button event handler to ${currentElement.id}");
|
120
|
+
});
|
121
|
+
|
122
|
+
console.debug(
|
123
|
+
"[Panda CMS] Dispatching event: pandaCmsPlainTextEditorLoaded"
|
124
|
+
);
|
125
|
+
|
126
|
+
// Let the parent know that the external resources have been loaded
|
127
|
+
this.frameDocument.dispatchEvent(new Event("pandaCmsPlainTextEditorLoaded"));
|
128
|
+
}
|
129
|
+
|
130
|
+
bindPlainTextSaveHandler(target) {
|
131
|
+
var blockContentId = target.getAttribute(
|
132
|
+
"data-editable-block-content-id"
|
133
|
+
);
|
134
|
+
|
135
|
+
if (target.getAttribute("data-editable-kind") == "html") { // Or markdown?
|
136
|
+
var content = target.innerText;
|
137
|
+
} else {
|
138
|
+
var content = target.innerHTML;
|
139
|
+
}
|
140
|
+
|
141
|
+
fetch(`/${this.adminPath}/pages/${this.pageId}/block_contents/${blockContentId}`, {
|
142
|
+
method: "PATCH",
|
143
|
+
headers: {
|
144
|
+
"Content-Type": "application/json",
|
145
|
+
"X-CSRF-Token": document
|
146
|
+
.querySelector('meta[name="csrf-token"]')
|
147
|
+
.getAttribute("content"),
|
148
|
+
},
|
149
|
+
body: JSON.stringify({ content: content }),
|
150
|
+
})
|
151
|
+
.then((response) => response.json())
|
152
|
+
.then((data) => {
|
153
|
+
this.stylePlainTextEditor(target, "success");
|
154
|
+
setTimeout(() => {
|
155
|
+
this.stylePlainTextEditor(target, "initial");
|
156
|
+
}, 1000);
|
157
|
+
})
|
158
|
+
.catch((error) => {
|
159
|
+
this.stylePlainTextEditor(target, "error");
|
160
|
+
setTimeout(() => {
|
161
|
+
this.stylePlainTextEditor(target, "initial");
|
162
|
+
}, 1000);
|
163
|
+
alert("Error:", error);
|
164
|
+
console.log(error);
|
165
|
+
});
|
166
|
+
}
|
167
|
+
|
168
|
+
/* Rich Text Editor Methods */
|
169
|
+
|
170
|
+
embedTrix() {
|
171
|
+
// Our stylesheet includes dist trix.css as of v2.1.7
|
172
|
+
this.addStylesheet(this.frameDocument, this.head, "/panda-cms-assets/rich_text_editor.css");
|
173
|
+
this.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/trix/dist/trix.js");
|
174
|
+
|
175
|
+
this.body.addEventListener("trix-before-initialize", () => {
|
176
|
+
// Change Trix.config if you need
|
177
|
+
console.debug("[Panda CMS] Trix before initialize");
|
178
|
+
// Trix.config.blockAttributes.heading = {
|
179
|
+
// tagName: "h2",
|
180
|
+
// terminal: true,
|
181
|
+
// breakOnReturn: true,
|
182
|
+
// group: false
|
183
|
+
// }
|
184
|
+
|
185
|
+
// Trix.config.blockAttributes.subHeading = {
|
186
|
+
// tagName: "h3",
|
187
|
+
// terminal: true,
|
188
|
+
// breakOnReturn: true,
|
189
|
+
// group: false
|
190
|
+
// }
|
191
|
+
|
192
|
+
// To make Trix styles appear we should define them in nb surely?
|
193
|
+
// Defining e.g. .panda-cms-content h2 in panda won't show it in panda, but
|
194
|
+
// as this is a page in nb
|
195
|
+
// We can define these styles through admin or a custom stylesheet eventually?
|
196
|
+
|
197
|
+
console.log(Trix.config.blockAttributes);
|
198
|
+
})
|
199
|
+
|
200
|
+
this.body.addEventListener("trix-initialize", (event) => {
|
201
|
+
const { toolbarElement } = event.target
|
202
|
+
const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]")
|
203
|
+
h1Button.insertAdjacentHTML("afterend", `
|
204
|
+
<button type="button" class="trix-button" data-trix-attribute="heading2" title="Heading 2" tabindex="-1" data-trix-active="">H2</button>
|
205
|
+
`)
|
206
|
+
})
|
207
|
+
|
208
|
+
document.getElementById('saveEditableButton').addEventListener('click', () => {
|
209
|
+
console.debug("[Panda CMS] Handling click event on #saveEditableButton");
|
210
|
+
// Grab each input element that's a rich text editor and append a save handler to the button
|
211
|
+
var elements = this.frameDocument.querySelectorAll("input[data-editor-type='rich-text']");
|
212
|
+
elements.forEach((element) => {
|
213
|
+
var blockContentId = element.getAttribute("data-editor-block-content-id");
|
214
|
+
this.bindTrixSaveHandler(blockContentId, element.value);
|
215
|
+
});
|
216
|
+
});
|
217
|
+
|
218
|
+
// This prevents the flash of content before the iFrame is ready
|
219
|
+
console.debug("[Panda CMS] Setting iFrame to visible");
|
220
|
+
this.setFrameVisible();
|
221
|
+
}
|
222
|
+
|
223
|
+
bindTrixSaveHandler(blockContentId, content) {
|
224
|
+
console.debug(`[Panda CMS] Calling save handler for ${blockContentId}...`);
|
225
|
+
fetch(`/${this.adminPath}/pages/${this.pageId}/block_contents/${blockContentId}`, {
|
226
|
+
method: "PATCH",
|
227
|
+
headers: {
|
228
|
+
"Content-Type": "application/json",
|
229
|
+
"X-CSRF-Token": this.csrfToken,
|
230
|
+
},
|
231
|
+
body: JSON.stringify({ content: content }),
|
232
|
+
})
|
233
|
+
.then((response) => response.json())
|
234
|
+
.then((data) => {
|
235
|
+
console.debug("[Panda CMS] Save successful for block content ID:", blockContentId);
|
236
|
+
document.getElementById('saveEditableButton').classList.remove("bg-inactive");
|
237
|
+
document.getElementById('saveEditableButton').classList.add("bg-active");
|
238
|
+
setTimeout(() => {
|
239
|
+
document.getElementById('saveEditableButton').classList.remove("bg-active");
|
240
|
+
document.getElementById('saveEditableButton').classList.add("bg-inactive");
|
241
|
+
}, 1500);
|
242
|
+
})
|
243
|
+
.catch((error) => {
|
244
|
+
console.log(error);
|
245
|
+
alert("Error updating. Please contact the support team!", error);
|
246
|
+
});
|
247
|
+
}
|
248
|
+
}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
// tailwindcss-stimulus-components@6.1.2 downloaded from https://ga.jspm.io/npm:tailwindcss-stimulus-components@6.1.2/dist/tailwindcss-stimulus-components.module.js
|
2
|
+
|
3
|
+
import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var V=(e,s,a)=>s in e?t(e,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[s]=a;var i=(e,t,s)=>V(e,typeof t!="symbol"?t+"":t,s);async function n(e,t,s={}){t?await T(e,s):await b(e,s)}async function T(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Enter",e,t);return v(e,{firstFrame(){e.classList.add(...s.split(" ")),e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.remove(...r.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" "))}})}async function b(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Leave",e,t);return v(e,{firstFrame(){e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.add(...s.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" ")),e.classList.add(...r.split(" "))}})}function C(e,t,s){return{transitionClasses:t.dataset[`transition${e}`]||s[e.toLowerCase()]||e.toLowerCase(),fromClasses:t.dataset[`transition${e}From`]||s[`${e.toLowerCase()}From`]||`${e.toLowerCase()}-from`,toClasses:t.dataset[`transition${e}To`]||s[`${e.toLowerCase()}To`]||`${e.toLowerCase()}-to`,toggleClass:t.dataset.toggleClass||s.toggleClass||s.toggle||"hidden"}}function L(e){e._stimulus_transition={timeout:null,interrupted:!1}}function I(e){e._stimulus_transition&&e._stimulus_transition.interrupt&&e._stimulus_transition.interrupt()}function v(e,t){e._stimulus_transition&&I(e);let s,a,o;return L(e),e._stimulus_transition.cleanup=()=>{a||t.firstFrame(),o||t.secondFrame(),t.ending(),e._stimulus_transition=null},e._stimulus_transition.interrupt=()=>{s=!0,e._stimulus_transition.timeout&&clearTimeout(e._stimulus_transition.timeout),e._stimulus_transition.cleanup()},new Promise((r=>{s||requestAnimationFrame((()=>{s||(t.firstFrame(),a=!0,requestAnimationFrame((()=>{s||(t.secondFrame(),o=!0,e._stimulus_transition&&(e._stimulus_transition.timeout=setTimeout((()=>{s||e._stimulus_transition.cleanup(),r()}),w(e))))})))}))}))}function w(e){let t=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,s=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;return t===0&&(t=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),t+s}var s=class extends e{connect(){setTimeout((()=>{T(this.element)}),this.showDelayValue),this.hasDismissAfterValue&&setTimeout((()=>{this.close()}),this.dismissAfterValue)}close(){b(this.element).then((()=>{this.element.remove()}))}};i(s,"values",{dismissAfter:Number,showDelay:{type:Number,default:0}});var a=class extends e{connect(){this.timeout=null}save(){clearTimeout(this.timeout),this.timeout=setTimeout((()=>{this.statusTarget.textContent=this.submittingTextValue,this.formTarget.requestSubmit()}),this.submitDurationValue)}success(){this.setStatus(this.successTextValue)}error(){this.setStatus(this.errorTextValue)}setStatus(e){this.statusTarget.textContent=e,this.timeout=setTimeout((()=>{this.statusTarget.textContent=""}),this.statusDurationValue)}};i(a,"targets",["form","status"]),i(a,"values",{submitDuration:{type:Number,default:1e3},statusDuration:{type:Number,default:2e3},submittingText:{type:String,default:"Saving..."},successText:{type:String,default:"Saved!"},errorText:{type:String,default:"Unable to save."}});var o=class extends e{update(){this.preview=this.colorTarget.value}set preview(e){this.previewTarget.style[this.styleValue]=e;let t=this._getContrastYIQ(e);this.styleValue==="color"?this.previewTarget.style.backgroundColor=t:this.previewTarget.style.color=t}_getContrastYIQ(e){e=e.replace("#","");let t=128,s=parseInt(e.substr(0,2),16),a=parseInt(e.substr(2,2),16),o=parseInt(e.substr(4,2),16);return(s*299+a*587+o*114)/1e3>=t?"#000":"#fff"}};i(o,"targets",["preview","color"]),i(o,"values",{style:{type:String,default:"backgroundColor"}});var r=class extends e{connect(){this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}openValueChanged(){n(this.menuTarget,this.openValue,this.transitionOptions),this.openValue===!0&&this.hasMenuItemTarget&&this.menuItemTargets[0].focus()}show(){this.openValue=!0}close(){this.openValue=!1}hide(e){this.closeOnClickOutsideValue&&e.target.nodeType&&this.element.contains(e.target)===!1&&this.openValue&&(this.openValue=!1),this.closeOnEscapeValue&&e.key==="Escape"&&this.openValue&&(this.openValue=!1)}toggle(){this.openValue=!this.openValue}nextItem(e){e.preventDefault(),this.menuItemTargets[this.nextIndex].focus()}previousItem(e){e.preventDefault(),this.menuItemTargets[this.previousIndex].focus()}get currentItemIndex(){return this.menuItemTargets.indexOf(document.activeElement)}get nextIndex(){return Math.min(this.currentItemIndex+1,this.menuItemTargets.length-1)}get previousIndex(){return Math.max(this.currentItemIndex-1,0)}get transitionOptions(){return{enter:this.hasEnterClass?this.enterClass:"transition ease-out duration-100",enterFrom:this.hasEnterFromClass?this.enterFromClass:"transform opacity-0 scale-95",enterTo:this.hasEnterToClass?this.enterToClass:"transform opacity-100 scale-100",leave:this.hasLeaveClass?this.leaveClass:"transition ease-in duration-75",leaveFrom:this.hasLeaveFromClass?this.leaveFromClass:"transform opacity-100 scale-100",leaveTo:this.hasLeaveToClass?this.leaveToClass:"transform opacity-0 scale-95",toggleClass:this.hasToggleClass?this.toggleClass:"hidden"}}beforeCache(){this.openValue=!1,this.menuTarget.classList.add("hidden")}};i(r,"targets",["menu","button","menuItem"]),i(r,"values",{open:{type:Boolean,default:!1},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}}),i(r,"classes",["enter","enterFrom","enterTo","leave","leaveFrom","leaveTo","toggle"]);var l=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.dialogTarget.show()}hide(){this.close()}beforeCache(){this.close()}};i(l,"targets",["dialog"]),i(l,"values",{open:Boolean});var u=class extends e{openValueChanged(){n(this.contentTarget,this.openValue),this.shouldAutoDismiss&&this.scheduleDismissal()}show(e){this.shouldAutoDismiss&&this.scheduleDismissal(),this.openValue=!0}hide(){this.openValue=!1}toggle(){this.openValue=!this.openValue}get shouldAutoDismiss(){return this.openValue&&this.hasDismissAfterValue}scheduleDismissal(){this.hasDismissAfterValue&&(this.cancelDismissal(),this.timeoutId=setTimeout((()=>{this.hide(),this.timeoutId=void 0}),this.dismissAfterValue))}cancelDismissal(){typeof this.timeoutId=="number"&&(clearTimeout(this.timeoutId),this.timeoutId=void 0)}};i(u,"targets",["content"]),i(u,"values",{dismissAfter:Number,open:{type:Boolean,default:!1}});var h=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache,document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.open()}hide(){this.close()}beforeCache(){this.close()}};i(h,"targets",["dialog"]),i(h,"values",{open:Boolean});var c=class extends e{initialize(){this.updateAnchorValue&&this.anchor&&(this.indexValue=this.tabTargets.findIndex((e=>e.id===this.anchor)))}connect(){this.showTab()}change(e){e.currentTarget.tagName==="SELECT"?this.indexValue=e.currentTarget.selectedIndex:e.currentTarget.dataset.index?this.indexValue=e.currentTarget.dataset.index:e.currentTarget.dataset.id?this.indexValue=this.tabTargets.findIndex((t=>t.id==e.currentTarget.dataset.id)):this.indexValue=this.tabTargets.indexOf(e.currentTarget)}nextTab(){this.indexValue=Math.min(this.indexValue+1,this.tabsCount-1)}previousTab(){this.indexValue=Math.max(this.indexValue-1,0)}firstTab(){this.indexValue=0}lastTab(){this.indexValue=this.tabsCount-1}indexValueChanged(){if(this.showTab(),this.dispatch("tab-change",{target:this.tabTargets[this.indexValue],detail:{activeIndex:this.indexValue}}),this.updateAnchorValue){let e=this.tabTargets[this.indexValue].id;if(this.scrollToAnchorValue)location.hash=e;else{let t=window.location.href.split("#")[0]+"#"+e;typeof Turbo<"u"?Turbo.navigator.history.replace(new URL(t)):history.replaceState({},document.title,t)}}}showTab(){this.panelTargets.forEach(((e,t)=>{let s=this.tabTargets[t];t===this.indexValue?(e.classList.remove("hidden"),s.ariaSelected="true",s.dataset.active=!0,this.hasInactiveTabClass&&s?.classList?.remove(...this.inactiveTabClasses),this.hasActiveTabClass&&s?.classList?.add(...this.activeTabClasses)):(e.classList.add("hidden"),s.ariaSelected=null,delete s.dataset.active,this.hasActiveTabClass&&s?.classList?.remove(...this.activeTabClasses),this.hasInactiveTabClass&&s?.classList?.add(...this.inactiveTabClasses))})),this.hasSelectTarget&&(this.selectTarget.selectedIndex=this.indexValue),this.scrollActiveTabIntoViewValue&&this.scrollToActiveTab()}scrollToActiveTab(){let e=this.element.querySelector("[aria-selected]");e&&e.scrollIntoView({inline:"center"})}get tabsCount(){return this.tabTargets.length}get anchor(){return document.URL.split("#").length>1?document.URL.split("#")[1]:null}};i(c,"classes",["activeTab","inactiveTab"]),i(c,"targets",["tab","panel","select"]),i(c,"values",{index:0,updateAnchor:Boolean,scrollToAnchor:Boolean,scrollActiveTabIntoView:Boolean});var d=class extends e{toggle(e){this.openValue=!this.openValue,this.animate()}toggleInput(e){this.openValue=e.target.checked,this.animate()}hide(){this.openValue=!1,this.animate()}show(){this.openValue=!0,this.animate()}animate(){this.toggleableTargets.forEach((e=>{n(e,this.openValue)}))}};i(d,"targets",["toggleable"]),i(d,"values",{open:{type:Boolean,default:!1}});export{s as Alert,a as Autosave,o as ColorPreview,r as Dropdown,l as Modal,u as Popover,h as Slideover,c as Tabs,d as Toggle,n as transition};
|
4
|
+
|
@@ -36,7 +36,7 @@ module PandaCms
|
|
36
36
|
def create_pages
|
37
37
|
@pages[:home] = PandaCms::Page.find_or_create_by!({path: "/", title: "Home", template: @templates[:homepage]})
|
38
38
|
@pages[:about] = PandaCms::Page.find_or_create_by!({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
|
39
|
-
@pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home]})
|
39
|
+
@pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home], status: "hidden"})
|
40
40
|
|
41
41
|
PandaCms::Page.reset_column_information
|
42
42
|
PandaCms::Page.rebuild!
|
data/app/lib/panda_cms/slug.rb
CHANGED
@@ -5,7 +5,7 @@ module PandaCms
|
|
5
5
|
#
|
6
6
|
# @param string [String] The provided string to turn into a slug
|
7
7
|
# @return string Generated slug
|
8
|
-
# @see
|
8
|
+
# @see slug_controller.js should also implement this logic
|
9
9
|
def self.generate(string)
|
10
10
|
# Trim whitespace and downcase the string
|
11
11
|
string = string.to_s.strip.downcase
|
@@ -16,12 +16,12 @@ module PandaCms
|
|
16
16
|
plain_text: "plain_text",
|
17
17
|
rich_text: "rich_text",
|
18
18
|
iframe: "iframe",
|
19
|
-
list: "list"
|
19
|
+
list: "list",
|
20
|
+
code: "code"
|
20
21
|
# image: "image",
|
21
22
|
# video: "video",
|
22
23
|
# audio: "audio",
|
23
24
|
# file: "file",
|
24
|
-
# code: "code",
|
25
25
|
# iframe: "iframe",
|
26
26
|
# quote: "quote",
|
27
27
|
# list: "list"
|
@@ -18,15 +18,21 @@ module PandaCms
|
|
18
18
|
has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "PandaCms::MenuItem", inverse_of: :page
|
19
19
|
has_many :menus, through: :menu_items
|
20
20
|
has_many :menus_of_parent, through: :parent, source: :menus
|
21
|
-
has_one :page_menu, foreign_key: :
|
21
|
+
has_one :page_menu, foreign_key: :start_page_id, class_name: "PandaCms::Menu"
|
22
22
|
|
23
23
|
validates :title, presence: true
|
24
|
+
|
24
25
|
validates :path,
|
25
26
|
presence: true,
|
26
27
|
uniqueness: true,
|
27
28
|
format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
|
28
|
-
|
29
|
-
validates :
|
29
|
+
|
30
|
+
validates :parent,
|
31
|
+
presence: true,
|
32
|
+
unless: -> { path == "/" }
|
33
|
+
|
34
|
+
validates :panda_cms_template_id,
|
35
|
+
presence: true
|
30
36
|
|
31
37
|
scope :ordered, -> { order(:lft) }
|
32
38
|
|
@@ -24,9 +24,11 @@ module PandaCms
|
|
24
24
|
validate :validate_template_file_exists
|
25
25
|
|
26
26
|
# Scopes
|
27
|
-
scope :ordered, -> { order(:sort_order) }
|
28
27
|
scope :available, -> { where("max_uses IS NULL OR (pages_count < max_uses)") }
|
29
|
-
|
28
|
+
|
29
|
+
def self.default
|
30
|
+
find_by(file_path: "layouts/page") || first
|
31
|
+
end
|
30
32
|
|
31
33
|
# Generate missing blocks for all templates
|
32
34
|
# @return [void]
|
@@ -2,7 +2,9 @@ module PandaCms
|
|
2
2
|
class User < ApplicationRecord
|
3
3
|
validates :firstname, presence: true
|
4
4
|
validates :lastname, presence: true
|
5
|
-
validates :email, presence: true, uniqueness:
|
5
|
+
validates :email, presence: true, uniqueness: true
|
6
|
+
|
7
|
+
before_save :downcase_email
|
6
8
|
|
7
9
|
def is_admin?
|
8
10
|
admin
|
@@ -15,5 +17,11 @@ module PandaCms
|
|
15
17
|
def self.for_select_list(scope = :all, order = {firstname: :asc, lastname: :asc})
|
16
18
|
PandaCms::User.send(scope).order(order).map { |u| [u.name, u.id] }
|
17
19
|
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def downcase_email
|
24
|
+
self.email = email.to_s.downcase
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
<%
|
1
|
+
<div class="" data-controller="dashboard">
|
2
|
+
<%= render PandaCms::Admin::ContainerComponent.new do |container| %>
|
3
|
+
<% container.with_heading(text: "Dashboard", level: 1) do |heading| %>
|
4
|
+
<% heading.with_button(action: :add, text: "Add Page", link: new_admin_page_path) %>
|
5
|
+
<% end %>
|
6
|
+
<dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
7
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Today", value: PandaCms::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
8
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Week", value: PandaCms::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
9
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Month", value: PandaCms::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
10
|
+
</dl>
|
4
11
|
<% end %>
|
5
|
-
|
6
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Today", value: PandaCms::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
7
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Week", value: PandaCms::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
8
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Month", value: PandaCms::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
9
|
-
</dl>
|
10
|
-
<% end %>
|
12
|
+
</div>
|
@@ -1,14 +1,13 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Add Page", level: 1) do |heading| %>
|
3
3
|
<% end %>
|
4
|
-
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
|
6
|
-
|
7
|
-
<div data-controller="
|
8
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
9
|
-
<%= f.select :parent_id, options, {}, { "data-
|
10
|
-
<%= f.text_field :title, { data: { "
|
11
|
-
<%= f.text_field :path, { data: { prefix: PandaCms::Current.root, "
|
5
|
+
<% options = nested_set_options(PandaCms::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
6
|
+
<div data-controller="slug">
|
7
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
8
|
+
<%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
|
9
|
+
<%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
|
10
|
+
<%= f.text_field :path, { data: { prefix: PandaCms::Current.root, "slug-target": "output_text" } } %>
|
12
11
|
<%= f.collection_select :panda_cms_template_id, PandaCms::Template.available, :id, :name %>
|
13
12
|
<%= f.button %>
|
14
13
|
</div>
|
@@ -1,8 +1,6 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Menus", level: 1) do |heading| %>
|
3
|
-
<% heading.with_button(action: :add, text: "Add Menu", link: new_admin_menu_path) %>
|
4
3
|
<% end %>
|
5
|
-
|
6
4
|
<%= render PandaCms::Admin::TableComponent.new(term: "menu", rows: menus) do |table| %>
|
7
5
|
<% table.column("Name") { |menu| link_to menu.name, edit_admin_menu_path(menu) } %>
|
8
6
|
<% table.column("Kind") { |menu| render PandaCms::Admin::TagComponent.new(status: :active, text: menu.kind.titleize) } %>
|
@@ -1,26 +1,28 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "#{page.title}", level: 1) %>
|
3
|
-
|
4
3
|
<% component.with_slideover(title: "Page Details") do %>
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_page_path, method: :put do |f| %>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
11
|
-
|
12
|
-
<%= f.submit "Save" %>
|
5
|
+
<%= f.text_field :title, class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
6
|
+
<%= f.text_field :template, value: template.name, readonly: true, class: "read-only:bg-gray-100 block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
7
|
+
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
8
|
+
<%= f.submit "Save" %>
|
13
9
|
<% end %>
|
14
10
|
<% end %>
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
<div class="grid grid-cols-2 mb-4 -mt-5">
|
12
|
+
<div>
|
13
|
+
<a class="inline-block mb-2 text-sm text-black/60" target="_blank" href="<%= @page.path %>"><%= @page.path %> <i class="ml-2 fa-solid fa-arrow-up-right-from-square"></i></a>
|
14
|
+
</div>
|
15
|
+
<div class="relative -mt-5">
|
16
|
+
<span class="absolute right-0"><%= render PandaCms::Admin::ButtonComponent.new(text: "Save Changes", action: :save_inactive, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %></span>
|
17
|
+
</div>
|
18
|
+
</div>
|
18
19
|
<iframe id="editablePageFrame" src="<%= page.path %>?embed_id=<%= page.id %>" class="p-0 m-0 w-full h-full border border-slate-200"></iframe>
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
<% end %>
|
21
|
+
<% content_for :head do %>
|
22
|
+
<%#= javascript_include_tag "panda_cms_editable", "data-turbo-track": "reload", defer: true %>
|
23
|
+
<!-- <script>
|
22
24
|
document.addEventListener("DOMContentLoaded", function() {
|
23
|
-
const editable = new
|
25
|
+
const editable = new PandaCmsEditableController("<%= page.id %>", document.getElementById("editablePageFrame"));
|
24
26
|
});
|
25
|
-
</script>
|
27
|
+
</script> -->
|
26
28
|
<% end %>
|
@@ -1,14 +1,13 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Add Page", level: 1) do |heading| %>
|
3
3
|
<% end %>
|
4
|
-
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
|
6
|
-
|
7
|
-
<div data-controller="
|
8
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
9
|
-
<%= f.select :parent_id, options, {}, { "data-
|
10
|
-
<%= f.text_field :title, { data: { "
|
11
|
-
<%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: PandaCms::Current.root, "
|
5
|
+
<% options = nested_set_options(PandaCms::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
6
|
+
<div data-controller="slug">
|
7
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
8
|
+
<%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
|
9
|
+
<%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
|
10
|
+
<%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: PandaCms::Current.root, "slug-target": "output_text" } } %>
|
12
11
|
<%= f.collection_select :panda_cms_template_id, PandaCms::Template.available, :id, :name %>
|
13
12
|
<%= f.button %>
|
14
13
|
</div>
|
@@ -1,8 +1,8 @@
|
|
1
1
|
<%= panda_cms_form_with model: post, url: url do |f| %>
|
2
|
-
<div data-controller="
|
3
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
4
|
-
<%= f.text_field :title, { required: true, data: { "
|
5
|
-
<%= f.text_field :slug, { required: true, data: { prefix: PandaCms::Current.root + "/#{PandaCms.posts[:prefix]}", "
|
2
|
+
<div data-controller="slug">
|
3
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
4
|
+
<%= f.text_field :title, { required: true, data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
|
5
|
+
<%= f.text_field :slug, { required: true, data: { prefix: PandaCms::Current.root + "/#{PandaCms.config.posts[:prefix]}", "slug-target": "output_text" } } %>
|
6
6
|
<%= f.select :user_id, PandaCms::User.for_select_list %>
|
7
7
|
<%= f.datetime_field :published_at, { required: true } %>
|
8
8
|
<%= f.select :status, PandaCms::Post.statuses.keys.map { |status| [status.humanize, status] } %>
|
@@ -3,10 +3,9 @@
|
|
3
3
|
<img src="/panda-cms-assets/panda-nav.png" class="py-2 mx-auto w-auto h-32">
|
4
4
|
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white"><%= t("panda_cms.admin.sessions.new.title") %></h2>
|
5
5
|
</div>
|
6
|
-
|
7
6
|
<% @providers.each do |provider| %>
|
8
7
|
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
9
|
-
<%= form_tag "#{PandaCms.
|
8
|
+
<%= form_tag "#{PandaCms.root_path}/auth/#{provider}", method: "post", data: {turbo: false} do %>
|
10
9
|
<input type="hidden" name="redirect_uri" value="<%= admin_login_callback_url(provider: provider) %>">
|
11
10
|
<button type="submit" id="button-sign-in-<%= provider %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white rounded-md border min-w-56 border-neutral-400">
|
12
11
|
<i class="fa-brands fa-<%= provider %> text-xl mr-1"></i>
|