webring-rails 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +598 -0
- data/app/assets/javascripts/webring/widget.js +224 -0
- data/app/controllers/webring/application_controller.rb +5 -0
- data/app/controllers/webring/members_controller.rb +60 -0
- data/app/controllers/webring/navigation_controller.rb +68 -0
- data/app/controllers/webring/widget_controller.rb +31 -0
- data/app/helpers/webring/application_helper.rb +4 -0
- data/app/jobs/webring/application_job.rb +4 -0
- data/app/mailers/webring/application_mailer.rb +6 -0
- data/app/models/concerns/webring/navigation.rb +60 -0
- data/app/models/webring/application_record.rb +5 -0
- data/app/models/webring/member.rb +31 -0
- data/config/routes.rb +8 -0
- data/lib/generators/USAGE +25 -0
- data/lib/generators/webring/controller/controller_generator.rb +49 -0
- data/lib/generators/webring/controller/templates/navigation_controller.rb +61 -0
- data/lib/generators/webring/install/install_generator.rb +31 -0
- data/lib/generators/webring/install/templates/AFTER_INSTALL +63 -0
- data/lib/generators/webring/member/member_generator.rb +55 -0
- data/lib/generators/webring/member/templates/AFTER_INSTALL +38 -0
- data/lib/generators/webring/member/templates/migration.rb +15 -0
- data/lib/generators/webring/member/templates/model.rb +31 -0
- data/lib/generators/webring/shared/route_injector.rb +40 -0
- data/lib/generators/webring_generator.rb +17 -0
- data/lib/webring/engine.rb +35 -0
- data/lib/webring/version.rb +3 -0
- data/lib/webring_rails.rb +5 -0
- metadata +115 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
/**
|
2
|
+
* Webring Navigation Widget
|
3
|
+
*
|
4
|
+
* Usage:
|
5
|
+
* <script src="https://yourhub.com/webring/widget.js" data-member-uid="YOUR_MEMBER_UID" data-widget-type="full"></script>
|
6
|
+
* <div id="webring-widget"></div>
|
7
|
+
*
|
8
|
+
* For multiple widgets on same page:
|
9
|
+
* <script src="https://yourhub.com/webring/widget.js" data-member-uid="YOUR_MEMBER_UID" data-widget-type="full" data-target-id="custom-widget-id"></script>
|
10
|
+
* <div id="custom-widget-id"></div>
|
11
|
+
*
|
12
|
+
* Widget Types:
|
13
|
+
* - full: text, back btn, random btn, forward btn (default)
|
14
|
+
* - no-text: back btn, random btn, forward btn (no text)
|
15
|
+
* - two-way: back btn, forward btn (no random)
|
16
|
+
* - one-way: forward btn only
|
17
|
+
*
|
18
|
+
* Additional Options:
|
19
|
+
* - data-button-text="true|false": If true, buttons will show text labels. If false, only symbols are shown. Default: true
|
20
|
+
* - data-styles="full|layout|none": Controls styling applied to the widget. Default: full
|
21
|
+
* - full: Apply all styles (default)
|
22
|
+
* - layout: Only layout styles, no visual design
|
23
|
+
* - none: No styles applied
|
24
|
+
*/
|
25
|
+
|
26
|
+
(function() {
|
27
|
+
// Configuration constants
|
28
|
+
const WIDGET_CONFIG = {
|
29
|
+
VALID_TYPES: ['full', 'no-text', 'two-way', 'one-way'],
|
30
|
+
DEFAULT_TYPE: 'full',
|
31
|
+
DEFAULT_TARGET_ID: 'webring-widget',
|
32
|
+
STYLE_ID: 'webring-widget-styles',
|
33
|
+
VALID_STYLE_TYPES: ['full', 'layout', 'none'],
|
34
|
+
DEFAULT_STYLE_TYPE: 'full'
|
35
|
+
};
|
36
|
+
|
37
|
+
const NAVIGATION_ACTIONS = {
|
38
|
+
prev: {
|
39
|
+
symbol: '«',
|
40
|
+
text: '« Prev',
|
41
|
+
title: 'Previous site',
|
42
|
+
path: 'previous'
|
43
|
+
},
|
44
|
+
random: {
|
45
|
+
symbol: '⚡',
|
46
|
+
text: 'Random',
|
47
|
+
title: 'Random site',
|
48
|
+
path: 'random'
|
49
|
+
},
|
50
|
+
next: {
|
51
|
+
symbol: '»',
|
52
|
+
text: 'Next »',
|
53
|
+
title: 'Next site',
|
54
|
+
path: 'next'
|
55
|
+
}
|
56
|
+
};
|
57
|
+
|
58
|
+
const WIDGET_TYPE_CONFIG = {
|
59
|
+
'full': { showTitle: true, actions: ['prev', 'random', 'next'] },
|
60
|
+
'no-text': { showTitle: false, actions: ['prev', 'random', 'next'] },
|
61
|
+
'two-way': { showTitle: false, actions: ['prev', 'next'] },
|
62
|
+
'one-way': { showTitle: false, actions: ['next'] }
|
63
|
+
};
|
64
|
+
|
65
|
+
// Define styles outside the function to avoid duplication
|
66
|
+
const STYLES = {
|
67
|
+
layout: `
|
68
|
+
.webring-nav {
|
69
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
70
|
+
font-size: 16px;
|
71
|
+
display: flex;
|
72
|
+
flex-direction: column;
|
73
|
+
align-items: center;
|
74
|
+
max-width: 350px;
|
75
|
+
margin: 0 auto;
|
76
|
+
padding: 10px;
|
77
|
+
}
|
78
|
+
.webring-title {
|
79
|
+
margin-bottom: 8px;
|
80
|
+
}
|
81
|
+
.webring-nav nav {
|
82
|
+
display: flex;
|
83
|
+
gap: 10px;
|
84
|
+
width: 100%;
|
85
|
+
justify-content: center;
|
86
|
+
}
|
87
|
+
.webring-nav a.webring-btn {
|
88
|
+
display: flex;
|
89
|
+
align-items: center;
|
90
|
+
justify-content: center;
|
91
|
+
padding: 6px 12px;
|
92
|
+
text-decoration: none;
|
93
|
+
}
|
94
|
+
.webring-nav[data-widget-type="no-text"] {
|
95
|
+
padding: 8px 10px;
|
96
|
+
}
|
97
|
+
.webring-nav[data-widget-type="one-way"] {
|
98
|
+
max-width: 200px;
|
99
|
+
}
|
100
|
+
.webring-nav[data-widget-type="one-way"] nav {
|
101
|
+
justify-content: center;
|
102
|
+
}
|
103
|
+
`,
|
104
|
+
design: `
|
105
|
+
.webring-nav[data-widget-type="full"] {
|
106
|
+
border: 2.5px solid #000000;
|
107
|
+
}
|
108
|
+
.webring-title {
|
109
|
+
font-weight: 600;
|
110
|
+
}
|
111
|
+
.webring-nav a.webring-btn {
|
112
|
+
color: #000000;
|
113
|
+
font-weight: 600;
|
114
|
+
background-color: #ffffff;
|
115
|
+
border: 2.5px solid #000000;
|
116
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
117
|
+
}
|
118
|
+
.webring-nav a.webring-btn:hover {
|
119
|
+
background-color: #000000;
|
120
|
+
color: #ffffff;
|
121
|
+
}
|
122
|
+
.webring-nav a.webring-btn:focus {
|
123
|
+
outline: none;
|
124
|
+
background-color: transparent;
|
125
|
+
color: #000000;
|
126
|
+
}
|
127
|
+
.webring-nav a.webring-btn:active {
|
128
|
+
border-color: #000000;
|
129
|
+
background-color: #000000;
|
130
|
+
color: #ffffff;
|
131
|
+
}
|
132
|
+
`
|
133
|
+
};
|
134
|
+
|
135
|
+
// Run immediately for widget initialization
|
136
|
+
createWidget();
|
137
|
+
|
138
|
+
function createWidget(scriptElement) {
|
139
|
+
const script = scriptElement || document.currentScript || (function() {
|
140
|
+
const scripts = document.getElementsByTagName('script');
|
141
|
+
return scripts[scripts.length - 1];
|
142
|
+
})();
|
143
|
+
|
144
|
+
if (!script) return;
|
145
|
+
|
146
|
+
// Config from data attributes
|
147
|
+
const memberUid = script.getAttribute('data-member-uid');
|
148
|
+
const widgetType = script.getAttribute('data-widget-type') || WIDGET_CONFIG.DEFAULT_TYPE;
|
149
|
+
const targetId = script.getAttribute('data-target-id') || WIDGET_CONFIG.DEFAULT_TARGET_ID;
|
150
|
+
const buttonText = script.getAttribute('data-button-text') !== 'false';
|
151
|
+
const stylesType = script.getAttribute('data-styles') || WIDGET_CONFIG.DEFAULT_STYLE_TYPE;
|
152
|
+
const stylesOption = WIDGET_CONFIG.VALID_STYLE_TYPES.includes(stylesType) ? stylesType : WIDGET_CONFIG.DEFAULT_STYLE_TYPE;
|
153
|
+
|
154
|
+
if (!memberUid) {
|
155
|
+
console.error('Webring Widget: Missing data-member-uid attribute on script tag.');
|
156
|
+
return;
|
157
|
+
}
|
158
|
+
|
159
|
+
if (!WIDGET_CONFIG.VALID_TYPES.includes(widgetType)) {
|
160
|
+
console.error(`Webring Widget: Invalid widget type "${widgetType}". Valid types: ${WIDGET_CONFIG.VALID_TYPES.join(', ')}`);
|
161
|
+
return;
|
162
|
+
}
|
163
|
+
|
164
|
+
const scriptSrc = script.getAttribute('src');
|
165
|
+
const baseUrl = new URL(scriptSrc, window.location.href).origin;
|
166
|
+
|
167
|
+
const renderWidget = function() {
|
168
|
+
const container = document.getElementById(targetId);
|
169
|
+
if (!container) {
|
170
|
+
console.error(`Webring Widget: No element with id "${targetId}" found.`);
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
|
174
|
+
// Navigation links
|
175
|
+
const config = WIDGET_TYPE_CONFIG[widgetType];
|
176
|
+
const linkElements = config.actions.map(action => {
|
177
|
+
const actionConfig = NAVIGATION_ACTIONS[action];
|
178
|
+
const url = `${baseUrl}/webring/${actionConfig.path}?source_member_uid=${memberUid}`;
|
179
|
+
const label = buttonText ? actionConfig.text : actionConfig.symbol;
|
180
|
+
return `<a href="${url}" title="${actionConfig.title}" class="webring-btn">${label}</a>`;
|
181
|
+
}).join('\n ');
|
182
|
+
|
183
|
+
// Create widget HTML
|
184
|
+
const title = config.showTitle ? '<span class="webring-title">Webring</span>' : '';
|
185
|
+
container.innerHTML = `
|
186
|
+
<div class="webring-nav" data-widget-type="${widgetType}">
|
187
|
+
${title}
|
188
|
+
<nav class="webring-buttons">
|
189
|
+
${linkElements}
|
190
|
+
</nav>
|
191
|
+
</div>
|
192
|
+
`;
|
193
|
+
|
194
|
+
applyStyles(stylesOption);
|
195
|
+
};
|
196
|
+
|
197
|
+
function applyStyles(styleOption) {
|
198
|
+
let styleElement = document.getElementById(WIDGET_CONFIG.STYLE_ID);
|
199
|
+
if (!styleElement) {
|
200
|
+
styleElement = document.createElement('style');
|
201
|
+
styleElement.id = WIDGET_CONFIG.STYLE_ID;
|
202
|
+
document.head.appendChild(styleElement);
|
203
|
+
}
|
204
|
+
|
205
|
+
switch (styleOption) {
|
206
|
+
case 'none':
|
207
|
+
styleElement.textContent = '';
|
208
|
+
break;
|
209
|
+
case 'layout':
|
210
|
+
styleElement.textContent = STYLES.layout;
|
211
|
+
break;
|
212
|
+
default: // 'full'
|
213
|
+
styleElement.textContent = STYLES.layout + STYLES.design;
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
// Run immediately or wait for DOM
|
218
|
+
if (document.readyState === 'loading') {
|
219
|
+
document.addEventListener('DOMContentLoaded', renderWidget);
|
220
|
+
} else {
|
221
|
+
renderWidget();
|
222
|
+
}
|
223
|
+
}
|
224
|
+
})();
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Webring
|
2
|
+
class MembersController < ::ApplicationController
|
3
|
+
before_action :set_member, only: %i[show edit update destroy]
|
4
|
+
|
5
|
+
# GET /webring/members
|
6
|
+
def index
|
7
|
+
@members = Member.all.order(created_at: :desc)
|
8
|
+
end
|
9
|
+
|
10
|
+
# GET /webring/members/1
|
11
|
+
def show; end
|
12
|
+
|
13
|
+
# GET /webring/members/new
|
14
|
+
def new
|
15
|
+
@member = Member.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# GET /webring/members/1/edit
|
19
|
+
def edit; end
|
20
|
+
|
21
|
+
# POST /webring/members
|
22
|
+
def create
|
23
|
+
@member = Member.new(member_params)
|
24
|
+
|
25
|
+
if @member.save
|
26
|
+
redirect_to admin_panel_member_path(@member), notice: 'Member was successfully created.'
|
27
|
+
else
|
28
|
+
render :new, status: :unprocessable_entity
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# PATCH/PUT /webring/members/1
|
33
|
+
def update
|
34
|
+
if @member.update(member_params)
|
35
|
+
redirect_to admin_panel_member_path(@member), notice: 'Member was successfully updated.'
|
36
|
+
else
|
37
|
+
render :edit, status: :unprocessable_entity
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# DELETE /webring/members/1
|
42
|
+
def destroy
|
43
|
+
@member.destroy
|
44
|
+
|
45
|
+
redirect_to admin_panel_members_url, notice: 'Member was successfully destroyed.'
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Use callbacks to share common setup or constraints between actions.
|
51
|
+
def set_member
|
52
|
+
@member = Member.find_by!(uid: params[:id])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Only allow a list of trusted parameters through.
|
56
|
+
def member_params
|
57
|
+
params.require(:member).permit(:name, :url)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Webring
|
2
|
+
class NavigationController < ApplicationController
|
3
|
+
before_action :find_member, only: %i[next previous]
|
4
|
+
before_action :ensure_members_exist, :check_member_exists, only: [:random]
|
5
|
+
|
6
|
+
# GET /webring/next
|
7
|
+
def next
|
8
|
+
member = Webring::Member.find_next(@member.uid)
|
9
|
+
|
10
|
+
redirect_to_member(member)
|
11
|
+
end
|
12
|
+
|
13
|
+
# GET /webring/previous
|
14
|
+
def previous
|
15
|
+
member = Webring::Member.find_previous(@member.uid)
|
16
|
+
|
17
|
+
redirect_to_member(member)
|
18
|
+
end
|
19
|
+
|
20
|
+
# GET /webring/random
|
21
|
+
def random
|
22
|
+
member = Webring::Member.find_random(source_member_uid: permitted_params[:source_member_uid])
|
23
|
+
|
24
|
+
redirect_to_member(member)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def permitted_params
|
30
|
+
params.permit(:source_member_uid)
|
31
|
+
end
|
32
|
+
|
33
|
+
def redirect_to_member(member)
|
34
|
+
redirect_to member.url, allow_other_host: true
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_member
|
38
|
+
@member = Webring::Member.find_by(uid: permitted_params[:source_member_uid])
|
39
|
+
return if @member
|
40
|
+
|
41
|
+
render_member_not_found
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_member_exists
|
45
|
+
member_uid = permitted_params[:source_member_uid]
|
46
|
+
return unless member_uid.present? && !Webring::Member.exists?(uid: member_uid)
|
47
|
+
|
48
|
+
render_member_not_found
|
49
|
+
end
|
50
|
+
|
51
|
+
def ensure_members_exist
|
52
|
+
return if Webring::Member.exists?
|
53
|
+
|
54
|
+
render plain: 'No members in the webring', status: :not_found
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_member_not_found
|
58
|
+
render plain: 'Member not found', status: :not_found
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_cors_headers
|
62
|
+
headers['Access-Control-Allow-Origin'] = '*'
|
63
|
+
headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
64
|
+
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept'
|
65
|
+
headers['Access-Control-Max-Age'] = '86400'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Webring
|
2
|
+
class WidgetController < ::ApplicationController
|
3
|
+
# Disable CSRF protection for widget.js as it needs to be loaded from other domains
|
4
|
+
skip_forgery_protection only: :show
|
5
|
+
|
6
|
+
# Set CORS headers for the widget
|
7
|
+
before_action :set_cors_headers, only: :show
|
8
|
+
|
9
|
+
# Serve the webring navigation widget JavaScript
|
10
|
+
# GET /webring/widget.js
|
11
|
+
def show
|
12
|
+
respond_to do |format|
|
13
|
+
format.js do
|
14
|
+
response.headers['Content-Type'] = 'application/javascript'
|
15
|
+
|
16
|
+
# Serve the JavaScript file from the engine's assets
|
17
|
+
render file: Webring::Engine.root.join('app/assets/javascripts/webring/widget.js')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def set_cors_headers
|
25
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
26
|
+
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
27
|
+
response.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept'
|
28
|
+
response.headers['Access-Control-Max-Age'] = '86400'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Webring
|
2
|
+
# Requires model to have `uid` and `created_at` columns
|
3
|
+
module Navigation
|
4
|
+
# Find the next member in the webring after the current one
|
5
|
+
# If current member is the last, return the first member (ring concept)
|
6
|
+
def find_next(source_member_uid)
|
7
|
+
source_member = find_by(uid: source_member_uid)
|
8
|
+
return first_member_by_creation unless source_member
|
9
|
+
|
10
|
+
find_next_member(source_member) || first_member_by_creation
|
11
|
+
end
|
12
|
+
|
13
|
+
# Find the previous member in the webring before the current one
|
14
|
+
# If current member is the first, return the last member (ring concept)
|
15
|
+
def find_previous(source_member_uid)
|
16
|
+
source_member = find_by(uid: source_member_uid)
|
17
|
+
return last_member_by_creation unless source_member
|
18
|
+
|
19
|
+
find_previous_member(source_member) || last_member_by_creation
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find a random member, excluding the current one if provided
|
23
|
+
# If current member is the only one, return it
|
24
|
+
def find_random(source_member_uid: nil)
|
25
|
+
return order('RANDOM()').first if source_member_uid.blank?
|
26
|
+
|
27
|
+
# Use exists? check to avoid loading records when not needed
|
28
|
+
excluded_scope = where.not(uid: source_member_uid)
|
29
|
+
|
30
|
+
if excluded_scope.exists?
|
31
|
+
excluded_scope.order('RANDOM()').first
|
32
|
+
else
|
33
|
+
# if only one member exists (the current one), return it
|
34
|
+
find_by(uid: source_member_uid)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def first_member_by_creation
|
41
|
+
order(created_at: :asc).first
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_member_by_creation
|
45
|
+
order(created_at: :desc).first
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_next_member(source_member)
|
49
|
+
where('created_at > ?', source_member.created_at)
|
50
|
+
.order(created_at: :asc)
|
51
|
+
.first
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_previous_member(source_member)
|
55
|
+
where('created_at < ?', source_member.created_at)
|
56
|
+
.order(created_at: :desc)
|
57
|
+
.first
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Webring
|
2
|
+
class Member < ApplicationRecord
|
3
|
+
extend Webring::Navigation
|
4
|
+
|
5
|
+
UID_LENGTH = 16 # 32-character hex string
|
6
|
+
|
7
|
+
validates :url, presence: true, uniqueness: true
|
8
|
+
validates :name, uniqueness: true, if: -> { name.present? }
|
9
|
+
validates :uid, presence: true, uniqueness: true, length: { is: UID_LENGTH * 2 }
|
10
|
+
|
11
|
+
before_validation :generate_uid, if: -> { uid.blank? }
|
12
|
+
before_validation :set_name_from_url, if: -> { name.blank? && url.present? }
|
13
|
+
|
14
|
+
def to_param
|
15
|
+
uid
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def generate_uid
|
21
|
+
loop do
|
22
|
+
self.uid = SecureRandom.hex(UID_LENGTH)
|
23
|
+
break unless self.class.exists?(uid: uid)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_name_from_url
|
28
|
+
self.name = url
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Description:
|
2
|
+
The webring generator creates the necessary structure for integrating webring
|
3
|
+
functionality into your Rails application.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
rails generate webring:install
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
config/initializers/webring.rb
|
10
|
+
# And mount the Webring engine in your routes.rb file
|
11
|
+
|
12
|
+
Example:
|
13
|
+
rails generate webring:member
|
14
|
+
|
15
|
+
This will create:
|
16
|
+
app/models/webring/member.rb
|
17
|
+
db/migrate/TIMESTAMP_create_webring_members.rb
|
18
|
+
# And add member routes to your routes.rb file
|
19
|
+
|
20
|
+
Example:
|
21
|
+
rails generate webring:controller
|
22
|
+
|
23
|
+
This will create:
|
24
|
+
app/controllers/webring/navigation_controller.rb
|
25
|
+
# And add navigation routes to your routes.rb file
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative '../shared/route_injector'
|
2
|
+
|
3
|
+
module Webring
|
4
|
+
module Generators
|
5
|
+
# @description The ControllerGenerator creates a NavigationController to handle webring navigation
|
6
|
+
# This generator creates both the controller file and adds the required routes
|
7
|
+
#
|
8
|
+
# @usage Run: rails generate webring:controller
|
9
|
+
#
|
10
|
+
# @example The generated controller provides three main navigation endpoints:
|
11
|
+
# # GET /webring/next - Navigate to the next site in the webring
|
12
|
+
# # GET /webring/previous - Navigate to the previous site in the webring
|
13
|
+
# # GET /webring/random - Navigate to a random site in the webring
|
14
|
+
#
|
15
|
+
# @note This generator should be run after installing the Webring engine and
|
16
|
+
# generating the Member model with webring:member
|
17
|
+
class ControllerGenerator < Rails::Generators::Base
|
18
|
+
include Shared::RouteInjector
|
19
|
+
|
20
|
+
source_root File.expand_path('templates', __dir__)
|
21
|
+
|
22
|
+
desc 'Creates a Webring::NavigationController and necessary routes for webring navigation'
|
23
|
+
|
24
|
+
# Creates the NavigationController file based on the template
|
25
|
+
# @return [void]
|
26
|
+
def create_controller_file
|
27
|
+
template 'navigation_controller.rb', 'app/controllers/webring/navigation_controller.rb'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds navigation routes to the application's routes.rb file
|
31
|
+
# These routes are used to navigate between webring members
|
32
|
+
# @return [void]
|
33
|
+
def create_navigation_routes
|
34
|
+
route_content = <<~ROUTE
|
35
|
+
# Webring navigation routes
|
36
|
+
namespace :webring do
|
37
|
+
root to: 'navigation#random'
|
38
|
+
|
39
|
+
get 'next', to: 'navigation#next'
|
40
|
+
get 'previous', to: 'navigation#previous'
|
41
|
+
get 'random', to: 'navigation#random'
|
42
|
+
end
|
43
|
+
ROUTE
|
44
|
+
|
45
|
+
inject_webring_routes(route_content)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Webring
|
2
|
+
class NavigationController < ApplicationController
|
3
|
+
before_action :find_member, only: %i[next previous]
|
4
|
+
before_action :ensure_members_exist, :check_member_exists, only: [:random]
|
5
|
+
|
6
|
+
# GET /webring/next
|
7
|
+
def next
|
8
|
+
member = Webring::Member.find_next(@member.uid)
|
9
|
+
|
10
|
+
redirect_to_member(member)
|
11
|
+
end
|
12
|
+
|
13
|
+
# GET /webring/previous
|
14
|
+
def previous
|
15
|
+
member = Webring::Member.find_previous(@member.uid)
|
16
|
+
|
17
|
+
redirect_to_member(member)
|
18
|
+
end
|
19
|
+
|
20
|
+
# GET /webring/random
|
21
|
+
def random
|
22
|
+
member = Webring::Member.find_random(source_member_uid: permitted_params[:source_member_uid])
|
23
|
+
|
24
|
+
redirect_to_member(member)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def permitted_params
|
30
|
+
params.permit(:source_member_uid)
|
31
|
+
end
|
32
|
+
|
33
|
+
def redirect_to_member(member)
|
34
|
+
redirect_to member.url, allow_other_host: true
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_member
|
38
|
+
@member = Webring::Member.find_by(uid: permitted_params[:source_member_uid])
|
39
|
+
return if @member
|
40
|
+
|
41
|
+
render_member_not_found
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_member_exists
|
45
|
+
member_uid = permitted_params[:source_member_uid]
|
46
|
+
return unless member_uid.present? && !Webring::Member.exists?(uid: member_uid)
|
47
|
+
|
48
|
+
render_member_not_found
|
49
|
+
end
|
50
|
+
|
51
|
+
def ensure_members_exist
|
52
|
+
return if Webring::Member.exists?
|
53
|
+
|
54
|
+
render plain: 'No members in the webring', status: :not_found
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_member_not_found
|
58
|
+
render plain: 'Member not found', status: :not_found
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Webring
|
2
|
+
module Generators
|
3
|
+
# @description The InstallGenerator is the first step to set up the Webring engine
|
4
|
+
# It mounts the Webring engine to your Rails application's routes
|
5
|
+
#
|
6
|
+
# @usage Run: rails generate webring:install
|
7
|
+
#
|
8
|
+
# @example After installation, the engine will be mounted at /webring
|
9
|
+
# # In your routes.rb
|
10
|
+
# mount Webring::Engine => '/webring', as: 'webring'
|
11
|
+
#
|
12
|
+
# @note You can change the mount path in your routes.rb file after installation
|
13
|
+
class InstallGenerator < Rails::Generators::Base
|
14
|
+
source_root File.expand_path('templates', __dir__)
|
15
|
+
|
16
|
+
desc 'Creates Webring routes and mounts the engine in your application.'
|
17
|
+
|
18
|
+
# Adds the engine mount point to the application's routes.rb file
|
19
|
+
# @return [void]
|
20
|
+
def add_webring_routes
|
21
|
+
route "mount Webring::Engine => '/webring', as: 'webring'\n\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Displays the README with next steps after installation
|
25
|
+
# @return [void]
|
26
|
+
def show_readme
|
27
|
+
readme 'AFTER_INSTALL' if behavior == :invoke
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|