Skip to content

Commit 2cdeaff

Browse files
authored
pretty permalinks (code4recovery#475)
* pretty permalinks * test and readme * refactor to fix navigation from controls * remove unnecessary setting
1 parent 0a0ddf9 commit 2cdeaff

File tree

11 files changed

+196
-162
lines changed

11 files changed

+196
-162
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ To use TSML UI on your website you only need to add some HTML to your web page.
1616

1717
You don't need to do anything other than enable HTTPS on your website. To ensure all users see this functionality, make sure that anyone who enters a `http://` address for your site is redirected to the `https://` address.
1818

19+
### Enable "pretty" permalinks
20+
21+
12 Step Meeting List sites have this enabled by default when they have a permalink structure like `/%postname%`. If you are not using 12 Step Meeting List and still want this functionality, this can be achieved by resolving your meeting detail pages to the index page. On a WordPress website this can be achieved by adding this PHP code to your theme's functions.php:
22+
23+
```php
24+
add_action('init', function () {
25+
add_rewrite_rule('^meetings/(.*)?', 'index.php?pagename=meetings', 'top');
26+
});
27+
```
28+
29+
Then add this parameter to your embed code: `data-path="/meetings"`.
30+
1931
### Add custom types
2032

2133
Here is an example of extending the `tsml_react_config` object to include a definition for an additional meeting type.

public/app.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {
33
RouteObject,
44
RouterProvider,
55
createBrowserRouter,
6+
createHashRouter,
67
useRouteError,
78
} from 'react-router-dom';
89

910
import { Global } from '@emotion/react';
10-
import { TsmlUI } from './components';
11+
import { Index, Meeting, TsmlUI } from './components';
1112
import { errorCss, globalCss } from './styles';
1213

1314
// locate element
@@ -17,12 +18,13 @@ export let routes: RouteObject[] = [];
1718
export let router: ReturnType<typeof createBrowserRouter>;
1819

1920
if (element) {
20-
const router = createBrowserRouter([
21+
const routes = [
2122
{
2223
path: '/*',
2324
element: (
2425
<TsmlUI
2526
google={element.getAttribute('data-google') || undefined}
27+
// eslint-disable-next-line no-undef
2628
settings={
2729
typeof tsml_react_config === 'undefined'
2830
? undefined
@@ -34,8 +36,40 @@ if (element) {
3436
/>
3537
),
3638
errorElement: <ErrorBoundary />,
39+
children: [
40+
{
41+
index: true,
42+
element: <Index />,
43+
},
44+
{
45+
path: ':slug',
46+
element: <Meeting />,
47+
},
48+
],
3749
},
38-
]);
50+
];
51+
52+
const basename = element.getAttribute('data-path');
53+
54+
// if landing on the page with the meeting param, redirect to that meeting
55+
const params = new URLSearchParams(window.location.search);
56+
const meeting = params.get('meeting');
57+
if (meeting) {
58+
params.delete('meeting');
59+
const query = params.toString();
60+
61+
const path = basename
62+
? `/${basename.replace(/^\//, '').replace(/\/$/, '')}/${meeting}${
63+
query ? `?${query}` : ''
64+
}`
65+
: `${window.location.pathname}#/${meeting}${query ? `?${query}` : ''}`;
66+
67+
window.history.replaceState({}, '', path);
68+
}
69+
70+
const router = basename
71+
? createBrowserRouter(routes, { basename })
72+
: createHashRouter(routes);
3973

4074
createRoot(element).render(<RouterProvider router={router} />);
4175
} else {

src/components/Link.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export default function Link({ meeting }: { meeting: Meeting }) {
2929

3030
return (
3131
<>
32-
<RouterLink to={formatUrl({ ...input, meeting: meeting.slug }, settings)}>
32+
<RouterLink
33+
to={formatUrl({ ...input, meeting: meeting.slug }, settings)}
34+
onClick={e => e.stopPropagation()}
35+
>
3336
{meeting.name}
3437
</RouterLink>
3538
{flags && <small>{flags}</small>}

src/components/Meeting.tsx

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useState } from 'react';
22

33
import { DateTime, Info } from 'luxon';
4-
import { Link as RouterLink } from 'react-router-dom';
4+
import { Link as RouterLink, useParams } from 'react-router-dom';
55

66
import {
77
formatDirectionsUrl,
@@ -27,15 +27,63 @@ import Map from './Map';
2727

2828
import { useData, useInput, useSettings } from '../hooks';
2929
import type { Meeting as MeetingType } from '../types';
30+
import Loading from './Loading';
31+
32+
export default function Meeting() {
33+
const { slug } = useParams();
34+
35+
const { capabilities, meetings, waitingForData } = useData();
36+
37+
const meeting = meetings[slug as string];
3038

31-
export default function Meeting({ meeting }: { meeting: MeetingType }) {
3239
const { settings, strings } = useSettings();
3340
const { input } = useInput();
3441

3542
// open types
3643
const [define, setDefine] = useState<string | undefined>();
3744

38-
const { capabilities, meetings } = useData();
45+
// scroll to top when you navigate to this page
46+
useEffect(() => {
47+
const el = document.getElementById('tsml-ui');
48+
if (el) {
49+
const headerHeight = Math.max(
50+
0,
51+
...[
52+
...Array.prototype.slice.call(
53+
document.body.getElementsByTagName('*')
54+
),
55+
]
56+
.filter(
57+
x =>
58+
getComputedStyle(x, null).getPropertyValue('position') ===
59+
'fixed' && x.offsetTop < 100
60+
)
61+
.map(x => x.offsetTop + x.offsetHeight)
62+
);
63+
if (headerHeight) {
64+
el.style.scrollMarginTop = `${headerHeight}px`;
65+
}
66+
el.scrollIntoView();
67+
}
68+
69+
document.getElementById('tsml-title')?.focus();
70+
71+
// log edit_url
72+
if (meeting?.edit_url) {
73+
console.log(`TSML UI edit ${meeting.name}: ${meeting.edit_url}`);
74+
wordPressEditLink(meeting.edit_url);
75+
}
76+
77+
return () => {
78+
wordPressEditLink();
79+
};
80+
}, [meeting]);
81+
82+
if (waitingForData) {
83+
return <Loading />;
84+
} else if (!meeting) {
85+
throw new Error('Meeting not found');
86+
}
3987

4088
const sharePayload = {
4189
title: meeting.name,
@@ -81,43 +129,6 @@ export default function Meeting({ meeting }: { meeting: MeetingType }) {
81129
return start.toFormat('cccc t ZZZZ');
82130
};
83131

84-
// scroll to top when you navigate to this page
85-
useEffect(() => {
86-
const el = document.getElementById('tsml-ui');
87-
if (el) {
88-
const headerHeight = Math.max(
89-
0,
90-
...[
91-
...Array.prototype.slice.call(
92-
document.body.getElementsByTagName('*')
93-
),
94-
]
95-
.filter(
96-
x =>
97-
getComputedStyle(x, null).getPropertyValue('position') ===
98-
'fixed' && x.offsetTop < 100
99-
)
100-
.map(x => x.offsetTop + x.offsetHeight)
101-
);
102-
if (headerHeight) {
103-
el.style.scrollMarginTop = `${headerHeight}px`;
104-
}
105-
el.scrollIntoView();
106-
}
107-
108-
document.getElementById('tsml-title')?.focus();
109-
110-
// log edit_url
111-
if (meeting.edit_url) {
112-
console.log(`TSML UI edit ${meeting.name}: ${meeting.edit_url}`);
113-
wordPressEditLink(meeting.edit_url);
114-
}
115-
116-
return () => {
117-
wordPressEditLink();
118-
};
119-
}, [meeting]);
120-
121132
// directions URL link
122133
const directionsUrl = meeting.isInPerson
123134
? formatDirectionsUrl(meeting)
@@ -255,7 +266,7 @@ export default function Meeting({ meeting }: { meeting: MeetingType }) {
255266
return (
256267
<div css={meetingCss}>
257268
<h1 id="tsml-title" tabIndex={-1}>
258-
<Link meeting={meeting} />
269+
{meeting.name}
259270
</h1>
260271
<div css={meetingBackCss}>
261272
<Icon icon="back" />

src/components/TsmlUI.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect } from 'react';
22

33
import { Global } from '@emotion/react';
4+
import { Outlet } from 'react-router-dom';
45

56
import {
67
DataProvider,
@@ -9,21 +10,11 @@ import {
910
InputProvider,
1011
SettingsProvider,
1112
useData,
12-
useFilter,
1313
useInput,
1414
} from '../hooks';
1515
import { globalCss } from '../styles';
1616

17-
import {
18-
Alert,
19-
Controls,
20-
DynamicHeight,
21-
Loading,
22-
Map,
23-
Meeting,
24-
Table,
25-
Title,
26-
} from './';
17+
import { Alert, Controls, DynamicHeight, Loading, Map, Table, Title } from './';
2718

2819
export default function TsmlUI({
2920
google,
@@ -56,7 +47,7 @@ export default function TsmlUI({
5647
<FilterProvider>
5748
<Global styles={globalCss} />
5849
<DynamicHeight>
59-
<Content />
50+
<Outlet />
6051
</DynamicHeight>
6152
</FilterProvider>
6253
</DataProvider>
@@ -66,14 +57,11 @@ export default function TsmlUI({
6657
);
6758
}
6859

69-
const Content = () => {
60+
export const Index = () => {
7061
const { waitingForData } = useData();
71-
const { meeting } = useFilter();
7262
const { input, waitingForInput } = useInput();
7363
return waitingForData ? (
7464
<Loading />
75-
) : meeting ? (
76-
<Meeting meeting={meeting} />
7765
) : (
7866
<>
7967
<Title />

src/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ export { default as Map } from './Map';
1010
export { default as Meeting } from './Meeting';
1111
export { default as Table } from './Table';
1212
export { default as Title } from './Title';
13-
export { default as TsmlUI } from './TsmlUI';
13+
export { Index, default as TsmlUI } from './TsmlUI';

src/helpers/format-url.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export function formatUrl(
66
) {
77
const query = {};
88

9+
const path = input.meeting ?? '';
10+
delete input.meeting;
11+
912
// region, time, type, and weekday
1013
settings.filters
1114
.filter(filter => typeof input[filter] !== 'undefined')
@@ -39,7 +42,5 @@ export function formatUrl(
3942

4043
const base = includeDomain ? `${window.location.origin}` : '';
4144

42-
const [path] = window.location.pathname.split('?');
43-
44-
return `${base}${path}${queryString.length ? `?${queryString}` : ''}`;
45+
return `${base}/${path}${queryString.length ? `?${queryString}` : ''}`;
4546
}

0 commit comments

Comments
 (0)