├── .github
└── ISSUE_TEMPLATE
│ ├── bugreport.yml
│ ├── config.yml
│ └── feature_request.yml
├── .gitignore
├── .prettierrc
├── .svnignore
├── 12-step-meeting-list.php
├── LICENSE.txt
├── SECURITY.md
├── assets
├── build
│ └── blocks
│ │ ├── block.json
│ │ ├── meetings.asset.php
│ │ ├── meetings.js
│ │ └── render.php
├── css
│ ├── admin.css
│ ├── admin.css.map
│ ├── admin.min.css
│ ├── admin.min.css.map
│ ├── public.css
│ ├── public.css.map
│ ├── public.min.css
│ └── public.min.css.map
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.svg
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── img
│ ├── apple.svg
│ ├── code4recovery.svg
│ ├── google.svg
│ ├── ionicons-minus-white.svg
│ ├── ionicons-minus.svg
│ ├── ionicons-plus-white.svg
│ ├── ionicons-plus.svg
│ └── venmo.svg
├── js
│ ├── admin.min.js
│ ├── admin.min.js.map
│ ├── bootstrap.dropdown.js
│ ├── closestmeetings.min.js
│ ├── jquery.validate.min.js
│ ├── public.min.js
│ └── public.min.js.map
└── src
│ ├── admin-import-settings.scss
│ ├── admin-meeting.scss
│ ├── admin.js
│ ├── admin.scss
│ ├── blocks
│ ├── block.json
│ ├── edit.js
│ ├── meetings.js
│ └── render.php
│ ├── jquery-ui.css
│ ├── maps.js
│ ├── mixins
│ └── breakpoints.scss
│ ├── public.js
│ ├── public.scss
│ └── tsml-jquery-ui.css
├── includes
├── admin_import.php
├── admin_lists.php
├── admin_log.php
├── admin_meeting.php
├── admin_menu.php
├── admin_region.php
├── admin_settings.php
├── ajax.php
├── blocks.php
├── blocks
│ └── class-tsml-blocks.php
├── filter_meetings.php
├── functions.php
├── functions_format.php
├── functions_get.php
├── functions_import.php
├── functions_input.php
├── functions_log.php
├── functions_timezone.php
├── init.php
├── save.php
├── shortcodes.php
├── variables.php
├── widgets.php
└── widgets_init.php
├── languages
└── 12-step-meeting-list.pot
├── mix-manifest.json
├── package-lock.json
├── package.json
├── readme.md
├── readme.txt
├── template.csv
├── templates
├── archive-meetings.php
├── archive-tsml-ui.php
├── footer.php
├── header.php
├── single-locations.php
└── single-meetings.php
├── uninstall.php
└── webpack.mix.js
/.github/ISSUE_TEMPLATE/bugreport.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report (Developers Only)
2 | description: For developers only, please. End-users should ask a question in our Discussions Q&A for support.
3 | title: '[Bug]: '
4 | labels: [bug]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this bug report!
10 | - type: input
11 | id: contact
12 | attributes:
13 | label: Contact Details
14 | description: How can we get in touch with you if we need more info?
15 | placeholder: ex. email@example.com
16 | validations:
17 | required: false
18 | - type: input
19 | id: website
20 | attributes:
21 | label: Website With Issue
22 | description: Please identify the website experiencing the bug
23 | placeholder: ex. https://example.com
24 | validations:
25 | required: false
26 | - type: textarea
27 | id: what-happened
28 | attributes:
29 | label: What happened?
30 | description: Also describe what you expect to happen
31 | placeholder: Tell us what you see!
32 | validations:
33 | required: true
34 | - type: dropdown
35 | id: browsers
36 | attributes:
37 | label: What browsers are you seeing the problem on?
38 | multiple: true
39 | options:
40 | - Firefox
41 | - Chrome
42 | - Safari
43 | - Microsoft Edge
44 | - type: textarea
45 | id: logs
46 | attributes:
47 | label: Relevant log output/errors
48 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
49 | render: shell
50 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request (Developers Only)
2 | description: For developers only, please. End-users should ask a question in our Discussions Q&A for support.
3 | title: '[Feature Request]: '
4 | labels: [new feature/enhancement]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this new feature/enhancement request!
10 | - type: input
11 | id: contact
12 | attributes:
13 | label: Contact Details
14 | description: How can we get in touch with you if we need more info?
15 | placeholder: ex. email@example.com
16 | validations:
17 | required: false
18 | - type: textarea
19 | id: request
20 | attributes:
21 | label: Requested Feature/Enhancement
22 | description: Explicitly state what you would like to see in TSML
23 | placeholder: Describe specifics and alternatives
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .svn
2 | .sass-cache
3 | node_modules
4 | .idea
5 | Dockerfile
6 | docker-compose.yml
7 | .vs
8 | .vscode
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "singleQuote": true,
4 | "printWidth": 140,
5 | "bracketSpacing": false,
6 | "useTabs": true
7 | }
8 |
--------------------------------------------------------------------------------
/.svnignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .git
3 | .gitignore
4 | .sass-cache
5 | node_modules
--------------------------------------------------------------------------------
/12-step-meeting-list.php:
--------------------------------------------------------------------------------
1 | ['class' => [], 'href' => [], 'title' => []],
18 | 'br' => [],
19 | 'code' => [],
20 | 'em' => [],
21 | 'pre' => [],
22 | 'span' => ['class' => []],
23 | 'small' => [],
24 | 'strong' => [],
25 | 'table' => ['style' => []],
26 | 'td' => [],
27 | 'tr' => []
28 | ]);
29 | define('TSML_GROUP_CONTACT_COUNT', 3);
30 | define('TSML_MEETING_GUIDE_APP_NOTIFY', 'appsupport@aa.org');
31 | define('TSML_MEETINGS_PERMISSION', 'edit_posts');
32 | define('TSML_PATH', plugin_dir_path(__FILE__));
33 | define('TSML_SETTINGS_PERMISSION', 'manage_options');
34 | define('TSML_VERSION', '3.17');
35 |
36 | // include these files first
37 | include TSML_PATH . '/includes/filter_meetings.php';
38 | include TSML_PATH . '/includes/functions.php';
39 | include TSML_PATH . '/includes/functions_format.php';
40 | include TSML_PATH . '/includes/functions_get.php';
41 | include TSML_PATH . '/includes/functions_import.php';
42 | include TSML_PATH . '/includes/functions_input.php';
43 | include TSML_PATH . '/includes/functions_log.php';
44 | include TSML_PATH . '/includes/functions_timezone.php';
45 | include TSML_PATH . '/includes/variables.php';
46 |
47 | // include public files
48 | include TSML_PATH . '/includes/ajax.php';
49 | include TSML_PATH . '/includes/init.php';
50 | include TSML_PATH . '/includes/shortcodes.php';
51 | include TSML_PATH . '/includes/widgets.php';
52 | include TSML_PATH . '/includes/widgets_init.php';
53 | include TSML_PATH . '/includes/blocks.php';
54 |
55 | // include admin files
56 | if (is_admin()) {
57 | include TSML_PATH . '/includes/admin_import.php';
58 | include TSML_PATH . '/includes/admin_lists.php';
59 | include TSML_PATH . '/includes/admin_log.php';
60 | include TSML_PATH . '/includes/admin_meeting.php';
61 | include TSML_PATH . '/includes/admin_menu.php';
62 | include TSML_PATH . '/includes/admin_region.php';
63 | include TSML_PATH . '/includes/admin_settings.php';
64 | include TSML_PATH . '/includes/save.php';
65 | }
66 |
67 | // these hooks need to be in this file
68 | register_activation_hook(__FILE__, 'tsml_plugin_activation');
69 | register_deactivation_hook(__FILE__, 'tsml_plugin_deactivation');
70 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Security Issues and Vulnerabilites
2 |
3 | The Code4Recovery team and community takes security bugs in our products seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
4 |
5 | To report a security issue, please use the [Security Tab](https://github.com/code4recovery/12-step-meeting-list/security), located under the repository name. If you cannot see the "Security" tab, select the ... dropdown menu, and then click Security. Please include as much information as possible, including steps to help our team recreate the issue.
6 |
7 | The Code4Recovery team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
8 |
--------------------------------------------------------------------------------
/assets/build/blocks/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schemas.wp.org/trunk/block.json",
3 | "apiVersion": 2,
4 | "name": "tsml/meetings",
5 | "title": "Meetings",
6 | "description": "12 Step Meeting List Meeting Block",
7 | "category": "embed",
8 | "icon": "groups",
9 | "attributes": {
10 | "alertBackgroundColor": {
11 | "type": "string",
12 | "default": "#faf4e0"
13 | },
14 | "alertTextColor": {
15 | "type": "string",
16 | "default": "#998a5e"
17 | },
18 | "backgroundColor": {
19 | "type": "string",
20 | "default": "#fff"
21 | },
22 | "borderRadius": {
23 | "type": "number",
24 | "default": 4
25 | },
26 | "focusColor": {
27 | "type": "string",
28 | "default": "#0d6efd40"
29 | },
30 | "fontFamily": {
31 | "type": "string",
32 | "default": "system-ui, -apple-system, sans-serif"
33 | },
34 | "inPersonBadgeColor": {
35 | "type": "string",
36 | "default": "#146c43"
37 | },
38 | "inactiveBadgeColor": {
39 | "type": "string",
40 | "default": "#b02a37"
41 | },
42 | "linkColor": {
43 | "type": "string",
44 | "default": "#0d6efd"
45 | },
46 | "onlineBadgeColor": {
47 | "type": "string",
48 | "default": "#0a58ca"
49 | },
50 | "onlineBackgroundImage": {
51 | "type": "string",
52 | "default": "url(https://images.unsplash.com/photo-1588196749597-9ff075ee6b5b?...)"
53 | },
54 | "textColor": {
55 | "type": "string",
56 | "default": "#212529"
57 | }
58 | },
59 | "supports": {
60 | "html": false,
61 | "align": true,
62 | "alignWide": true,
63 | "anchor": true,
64 | "className": true,
65 | "spacing": {
66 | "margin": true,
67 | "padding": false,
68 | "blockGap": false
69 | },
70 | "typography": {
71 | "fontSize": true,
72 | "__experimentalFontFamily": true,
73 | "__experimentalFontStyle": true,
74 | "__experimentalFontWeight": true,
75 | "__experimentalTextTransform": true
76 | }
77 | },
78 | "textdomain": "12-step-meeting-list",
79 | "render": "file:./render.php",
80 | "editorScript": "file:./meetings.js"
81 | }
--------------------------------------------------------------------------------
/assets/build/blocks/meetings.asset.php:
--------------------------------------------------------------------------------
1 | array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-i18n'), 'version' => '6e2ecec5a6d6ae712cdb');
2 |
--------------------------------------------------------------------------------
/assets/build/blocks/meetings.js:
--------------------------------------------------------------------------------
1 | (()=>{"use strict";const e=window.wp.blocks,t=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"tsml/meetings","title":"Meetings","description":"12 Step Meeting List Meeting Block","category":"embed","icon":"groups","attributes":{"alertBackgroundColor":{"type":"string","default":"#faf4e0"},"alertTextColor":{"type":"string","default":"#998a5e"},"backgroundColor":{"type":"string","default":"#fff"},"borderRadius":{"type":"number","default":4},"focusColor":{"type":"string","default":"#0d6efd40"},"fontFamily":{"type":"string","default":"system-ui, -apple-system, sans-serif"},"inPersonBadgeColor":{"type":"string","default":"#146c43"},"inactiveBadgeColor":{"type":"string","default":"#b02a37"},"linkColor":{"type":"string","default":"#0d6efd"},"onlineBadgeColor":{"type":"string","default":"#0a58ca"},"onlineBackgroundImage":{"type":"string","default":"url(https://images.unsplash.com/photo-1588196749597-9ff075ee6b5b?...)"},"textColor":{"type":"string","default":"#212529"}},"supports":{"html":false,"align":true,"alignWide":true,"anchor":true,"className":true,"spacing":{"margin":true,"padding":false,"blockGap":false},"typography":{"fontSize":true,"__experimentalFontFamily":true,"__experimentalFontStyle":true,"__experimentalFontWeight":true,"__experimentalTextTransform":true}},"textdomain":"12-step-meeting-list","render":"file:./render.php","editorScript":"file:./meetings.js"}'),l=window.wp.blockEditor,o=window.wp.components,n=window.wp.data,i=window.wp.i18n,r=window.ReactJSXRuntime;(0,e.registerBlockType)(t,{edit:({attributes:e,setAttributes:t})=>{const{alertBackgroundColor:s,alertTextColor:a,backgroundColor:d,borderRadius:c,focusColor:g,fontFamily:p,fontSize:m,inPersonBadgeColor:u,inactiveBadgeColor:x,linkColor:h,onlineBadgeColor:j,onlineBackgroundImage:y,textColor:C}=e,f=(0,n.useSelect)("core/block-editor").getSettings().colors,{serverSideRender:k}=((0,l.useBlockProps)({style:{"--alert-background":s,"--alert-text":a,"--background":d,"--border-radius":c+"px","--focus":g,"--font-family":p,"--font-size":m+"px","--in-person":u,"--inactive":x,"--link":h,"--online":j,"--online-background-image":`url(${y})`,"--text":C}}),wp),_={fontSize:"11px",fontWeight:"500",lineHeight:"1.4",textTransform:"uppercase",display:"block",padding:"0",marginBottom:"1.5em"},B={marginTop:"-.5em",marginBottom:"1.5em",fontSize:"11px"};return(0,r.jsxs)("div",{...(0,l.useBlockProps)(),children:[(0,r.jsxs)(l.InspectorControls,{group:"styles",children:[(0,r.jsxs)(o.PanelBody,{title:(0,i.__)("Backgrounds","12-step-meeting-list"),initialOpen:!1,children:[(0,r.jsxs)("div",{style:{marginTop:"1.5em"},children:[(0,r.jsx)("legend",{style:{..._},children:"Background color"}),(0,r.jsx)("p",{style:{...B},children:(0,r.jsx)("em",{children:"Applies to entire meeting list block."})}),(0,r.jsx)(o.ColorPalette,{value:d,colors:[...f],onChange:e=>t({backgroundColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Online Background image"}),(0,r.jsx)("p",{style:{...B},children:(0,r.jsx)("em",{children:"Will be shown instead of a map for online meetings (approx 2000px x 2000px)."})}),(0,r.jsx)(l.MediaUpload,{onSelect:e=>t({onlineBackgroundImage:e.url}),allowedTypes:["image"],value:y,render:({open:e})=>(0,r.jsx)(o.Button,{onClick:e,variant:"primary",help:"test",children:(0,i.__)("Select Background Image","12-step-meeting-list")})}),y&&(0,r.jsx)(o.Button,{onClick:()=>t({onlineBackgroundImage:""}),variant:"secondary",style:{marginTop:"10px",marginBottom:"1.5em"},children:(0,i.__)("Remove Background Image","12-step-meeting-list")})]})]}),(0,r.jsxs)(o.PanelBody,{title:(0,i.__)("Text Colors","12-step-meeting-list"),initialOpen:!1,children:[(0,r.jsxs)("div",{style:{marginTop:"1.5em"},children:[(0,r.jsx)("legend",{style:{..._},children:"Text color"}),(0,r.jsx)(o.ColorPalette,{value:C,colors:[...f],onChange:e=>t({textColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Link color"}),(0,r.jsx)(o.ColorPalette,{value:h,colors:[...f],onChange:e=>t({linkColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Input focus shadow color"}),(0,r.jsx)(o.ColorPalette,{enableAlpha:!0,value:g,colors:[...f],onChange:e=>t({focusColor:e})})]})]}),(0,r.jsxs)(o.PanelBody,{title:(0,i.__)("Alert Colors","12-step-meeting-list"),initialOpen:!1,children:[(0,r.jsxs)("div",{style:{marginTop:"1.5em"},children:[(0,r.jsx)("legend",{style:{..._},children:"Background color"}),(0,r.jsx)(o.ColorPalette,{value:s,colors:[...f],onChange:e=>t({alertBackgroundColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Text color"}),(0,r.jsx)(o.ColorPalette,{value:a,colors:[...f],onChange:e=>t({alertTextColor:e})})]})]}),(0,r.jsxs)(o.PanelBody,{title:(0,i.__)("Badge Colors","12-step-meeting-list"),initialOpen:!1,children:[(0,r.jsxs)("div",{style:{marginTop:"1.5em"},children:[(0,r.jsx)("legend",{style:{..._},children:"In person meeting"}),(0,r.jsx)(o.ColorPalette,{value:u,colors:[...f],onChange:e=>t({inPersonBadgeColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Inactive meeting"}),(0,r.jsx)(o.ColorPalette,{value:x,colors:[...f],onChange:e=>t({inactiveBadgeColor:e})})]}),(0,r.jsx)(o.__experimentalDivider,{}),(0,r.jsxs)("div",{children:[(0,r.jsx)("legend",{style:{..._},children:"Online meeting"}),(0,r.jsx)(o.ColorPalette,{label:(0,i.__)("Online badge color","12-step-meeting-list"),value:j,colors:[...f],onChange:e=>t({onlineBadgeColor:e})})]})]}),(0,r.jsx)(o.PanelBody,{title:(0,i.__)("Border","12-step-meeting-list"),initialOpen:!1,children:(0,r.jsx)("div",{style:{marginTop:"1.5em"},children:(0,r.jsx)(o.RangeControl,{label:(0,i.__)("Border Radius (px)","12-step-meeting-list"),value:c,onChange:e=>t({borderRadius:e}),min:0,max:50})})})]}),(0,r.jsx)(o.Placeholder,{icon:(0,r.jsx)(l.BlockIcon,{icon:"groups",size:"50"}),label:(0,i.__)("Meetings","12-step-meeting-list"),instructions:(0,i.__)("View the page to see the block. it's recommended not to put any page content below the block, and to make the block as wide as possible.","12-step-meeting-list")}),(0,r.jsx)(k,{block:"tsml/meetings",attributes:e})]})}})})();
--------------------------------------------------------------------------------
/assets/build/blocks/render.php:
--------------------------------------------------------------------------------
1 | $attributes['backgroundColor'] ?? null,
28 | '--alert-background' => $attributes['alertBackgroundColor'] ?? null,
29 | '--alert-text' => $attributes['alertTextColor'] ?? null,
30 | '--in-person' => $attributes['inPersonBadgeColor'] ?? null,
31 | '--inactive' => $attributes['inactiveBadgeColor'] ?? null,
32 | '--link' => $attributes['linkColor'] ?? null,
33 | '--online' => $attributes['onlineBadgeColor'] ?? null,
34 | '--text' => $attributes['textColor'] ?? null,
35 | '--focus' => $attributes['focusColor'] ?? null,
36 | '--border-radius' => isset($attributes['borderRadius']) ? $attributes['borderRadius'].'px' : null,
37 | '--font-family' => isset($attributes['fontFamily']) ? 'var(--wp--preset--font-family--'.$attributes['fontFamily'].')' : null,
38 | '--online-background-image' => isset($attributes['onlineBackgroundImage']) ? 'url('.$attributes['onlineBackgroundImage'].')' : null,
39 | "--font-size" => $size ?? null,
40 | ];
41 |
42 | /** Load TSML assets */
43 | tsml_assets();
44 |
45 | /** Loop through styles & output inline ';
53 | ?>
54 |
55 |
>
56 |
57 |
58 |
--------------------------------------------------------------------------------
/assets/css/admin.css:
--------------------------------------------------------------------------------
1 | .ui-autocomplete{border-radius:4px;box-shadow:0 6px 12px rgba(0,0,0,.175);font-family:inherit;font-size:.8em;min-width:270px}.ui-autocomplete-category{border-bottom:1px solid #ccc;font-size:.8em;font-weight:700;margin:10px 0 0;padding:6px 20px;text-transform:uppercase}.ui-autocomplete-highlight{font-weight:700!important}.ui-menu-item{padding:6px 20px!important}
2 | body .wrap.tsml_admin_settings h1{margin:0;padding:0}body .wrap.tsml_admin_settings h2{font-size:20px;margin:0}body .wrap.tsml_admin_settings .h3,body .wrap.tsml_admin_settings h3{font-weight:700;margin-top:10px!important}body .wrap.tsml_admin_settings label,body .wrap.tsml_admin_settings p,body .wrap.tsml_admin_settings ul{font-size:15px}body .wrap.tsml_admin_settings .notice{margin-bottom:20px}body .wrap.tsml_admin_settings .notice.notice-error{padding:10px 10px 8px 45px}body .wrap.tsml_admin_settings .notice.notice-error li{margin:0 0 2px}body .wrap.tsml_admin_settings .notice.notice-warning{padding-bottom:20px;padding-top:20px}body .wrap.tsml_admin_settings .log-table{margin:0 -20px!important;width:calc(100% + 40px)!important}body .wrap.tsml_admin_settings .log-table--large{font-size:1rem}body .wrap.tsml_admin_settings .log-table thead{background:#f0f0f0;position:sticky;top:2rem;z-index:30}body .wrap.tsml_admin_settings .log-table td,body .wrap.tsml_admin_settings .log-table th{padding:10px 20px!important;text-align:left!important}body .wrap.tsml_admin_settings .log-table .error td{color:#b00}body .wrap.tsml_admin_settings .log-table__empty{display:none}body .wrap.tsml_admin_settings .log-table__empty td{font-family:inherit!important;padding:1rem!important;text-align:center!important}body .wrap.tsml_admin_settings a.button-large{align-items:center;display:inline-flex;font-size:15px;gap:6px;padding:4px 12px!important}body .wrap.tsml_admin_settings .progress{background-color:#fff;box-shadow:0 1px 1px 0 rgba(0,0,0,.1);height:20px;overflow:hidden;width:100%}body .wrap.tsml_admin_settings .progress .progress-bar{background-color:#bbb;color:#fff;float:left;font-size:12px;height:100%;line-height:20px;overflow:hidden;text-align:center;transition:width .6s ease;width:0}body .wrap.tsml_admin_settings .three-column{display:flex;flex-direction:column;gap:20px;width:100%}@media (min-width:1200px){body .wrap.tsml_admin_settings .three-column{align-items:flex-start;flex-direction:row}body .wrap.tsml_admin_settings .three-column>*{flex:1}}body .wrap.tsml_admin_settings .stack{display:grid;gap:20px;justify-items:flex-start}body .wrap.tsml_admin_settings .stack.compact{gap:8px}body .wrap.tsml_admin_settings .wrap{margin:0;padding:20px 20px 0 0}body .wrap.tsml_admin_settings .row{display:flex;flex-wrap:wrap;gap:9px}body .wrap.tsml_admin_settings .postbox{box-sizing:border-box;margin:0;padding:20px;width:100%}body .wrap.tsml_admin_settings .postbox *{margin:0}body .wrap.tsml_admin_settings .postbox .logo{margin-top:-30px}body .wrap.tsml_admin_settings .postbox details summary{cursor:pointer;font-size:1rem;font-weight:700}body .wrap.tsml_admin_settings .postbox details p{margin:10px 0 0}body .wrap.tsml_admin_settings .postbox ul{margin:10px 0 0 20px}body .wrap.tsml_admin_settings .postbox ul li+li{margin-top:10px}body .wrap.tsml_admin_settings .postbox ul.types{-moz-column-gap:40px;column-gap:40px;-moz-columns:2 auto;column-count:2;line-height:1.4;margin:20px 0}body .wrap.tsml_admin_settings .postbox select{width:100%}body .wrap.tsml_admin_settings .postbox form input{box-shadow:none}body .wrap.tsml_admin_settings .postbox form input[type=email],body .wrap.tsml_admin_settings .postbox form input[type=text],body .wrap.tsml_admin_settings .postbox form input[type=url]{font-family:monospace;width:300px}body .wrap.tsml_admin_settings .postbox form input[type=submit]{min-width:80px}body .wrap.tsml_admin_settings .postbox form select{width:300px}body .wrap.tsml_admin_settings .postbox form.radio label{display:flex;gap:.25rem;justify-content:flex-start}body .wrap.tsml_admin_settings .postbox form.radio label input[type=radio]{margin-top:3px}body .wrap.tsml_admin_settings .postbox table{border:0;border-collapse:collapse;border-spacing:0;margin:0 0 10px;width:100%}body .wrap.tsml_admin_settings .postbox table tr td,body .wrap.tsml_admin_settings .postbox table tr th{border:0;margin:0;padding:4px 0;text-align:left}body .wrap.tsml_admin_settings .postbox table tr td input.button,body .wrap.tsml_admin_settings .postbox table tr th input.button{margin-right:10px}body .wrap.tsml_admin_settings .postbox table tr td.align-center,body .wrap.tsml_admin_settings .postbox table tr th.align-center{text-align:center}body .wrap.tsml_admin_settings .postbox table tr td.align-right,body .wrap.tsml_admin_settings .postbox table tr th.align-right{text-align:right}body .wrap.tsml_admin_settings .postbox table tr td.small,body .wrap.tsml_admin_settings .postbox table tr th.small{width:1%}body .wrap.tsml_admin_settings .postbox table tr td:first-child,body .wrap.tsml_admin_settings .postbox table tr th:first-child{font-family:monospace}body .wrap.tsml_admin_settings .postbox table tr td:last-child,body .wrap.tsml_admin_settings .postbox table tr th:last-child{text-align:right}body .wrap.tsml_admin_settings .postbox table tr td:last-child span,body .wrap.tsml_admin_settings .postbox table tr th:last-child span{cursor:pointer}body .wrap.tsml_admin_settings .postbox table tr td{border-top:1px solid #ddd}body .wrap.tsml_admin_settings .postbox table tr:last-child td{padding-bottom:0}body .wrap.tsml_admin_settings .postbox table form .button.button-small{height:18px;line-height:16px}body .wrap.tsml_admin_settings .postbox#try_the_apps p.buttons{margin-left:-5px;margin-right:-5px;overflow:auto}body .wrap.tsml_admin_settings .postbox#try_the_apps p.buttons a{box-sizing:border-box;display:block;float:left;padding:0 5px;width:50%}body .wrap.tsml_admin_settings .postbox#try_the_apps p.buttons a img{display:block;height:auto;width:100%}body.taxonomy-tsml_region .row-actions{display:none}body.post-type-tsml_meeting div.notice ul{list-style:disc;padding-left:1rem}body.post-type-tsml_meeting .postbox .handlediv{display:none}body.post-type-tsml_meeting .postbox .hndle{background-color:#fafafa}body.post-type-tsml_meeting .postbox.closed .inside{display:block}body.post-type-tsml_meeting .postbox h2 small,body.post-type-tsml_meeting .postbox h3 small{color:#aaa;font-size:inherit}body.post-type-tsml_meeting .postbox h2 small:before,body.post-type-tsml_meeting .postbox h3 small:before{content:" ("}body.post-type-tsml_meeting .postbox h2 small:after,body.post-type-tsml_meeting .postbox h3 small:after{content:")"}body.post-type-tsml_meeting .meta_form_separator{margin-left:15%}body.post-type-tsml_meeting .meta_form_separator h4{border-bottom:1px solid #ccc;margin-bottom:0;padding-bottom:10px}body.post-type-tsml_meeting .meta_form_separator p{margin:10px 0 0}body.post-type-tsml_meeting .meta_form_row{clear:left;overflow:auto;padding:10px 0 0}body.post-type-tsml_meeting .meta_form_row #map,body.post-type-tsml_meeting .meta_form_row div.checkboxes,body.post-type-tsml_meeting .meta_form_row input,body.post-type-tsml_meeting .meta_form_row label{float:left;line-height:100%}body.post-type-tsml_meeting .meta_form_row label{font-size:15px;line-height:1.4;margin-right:2%;margin-top:9px;text-align:right;white-space:nowrap;width:13%}body.post-type-tsml_meeting .meta_form_row #map,body.post-type-tsml_meeting .meta_form_row div.checkboxes,body.post-type-tsml_meeting .meta_form_row input[type=date],body.post-type-tsml_meeting .meta_form_row input[type=email],body.post-type-tsml_meeting .meta_form_row input[type=text],body.post-type-tsml_meeting .meta_form_row input[type=url],body.post-type-tsml_meeting .meta_form_row select,body.post-type-tsml_meeting .meta_form_row textarea{box-sizing:border-box;float:left;font-size:18px;height:40px;line-height:normal;margin:0;padding:6px;width:85%}body.post-type-tsml_meeting .meta_form_row #map,body.post-type-tsml_meeting .meta_form_row input[type=date],body.post-type-tsml_meeting .meta_form_row input[type=text],body.post-type-tsml_meeting .meta_form_row select,body.post-type-tsml_meeting .meta_form_row textarea{border:1px solid #ddd!important;border-radius:4px!important}body.post-type-tsml_meeting .meta_form_row input.time{width:42%}body.post-type-tsml_meeting .meta_form_row input.time:last-child{margin-left:1%}body.post-type-tsml_meeting .meta_form_row small{display:none;font-size:.8rem;margin-left:15%;margin-top:.25rem}body.post-type-tsml_meeting .meta_form_row small.error_message{color:#d40047}body.post-type-tsml_meeting .meta_form_row small.warning_message{color:#a14c00}body.post-type-tsml_meeting .meta_form_row small.show{display:inline-block}body.post-type-tsml_meeting .meta_form_row input,body.post-type-tsml_meeting .meta_form_row select{border:1px solid #ccc}body.post-type-tsml_meeting .meta_form_row input[type=checkbox],body.post-type-tsml_meeting .meta_form_row input[type=radio]{box-shadow:none}body.post-type-tsml_meeting .meta_form_row input[type=checkbox]:checked:before,body.post-type-tsml_meeting .meta_form_row input[type=radio]:checked:before{display:none}body.post-type-tsml_meeting .meta_form_row input[type=checkbox]{-webkit-appearance:checkbox}body.post-type-tsml_meeting .meta_form_row input[type=radio]{-webkit-appearance:radio}body.post-type-tsml_meeting .meta_form_row input[disabled]{cursor:not-allowed;opacity:.5}body.post-type-tsml_meeting .meta_form_row input.error{border:1px solid #d40047!important}body.post-type-tsml_meeting .meta_form_row input.warning{border:1px solid #d3a73e!important}body.post-type-tsml_meeting .meta_form_row.checkbox,body.post-type-tsml_meeting .meta_form_row.radio{line-height:1;padding-left:15%}body.post-type-tsml_meeting .meta_form_row.checkbox label,body.post-type-tsml_meeting .meta_form_row.radio label{display:block;float:none;font-size:13px;margin:0;padding:0 0 0 20px;position:relative;text-align:left;width:auto}body.post-type-tsml_meeting .meta_form_row.checkbox label input,body.post-type-tsml_meeting .meta_form_row.radio label input{left:0;position:absolute;top:5px}body.post-type-tsml_meeting .meta_form_row.checkbox label:first-child,body.post-type-tsml_meeting .meta_form_row.radio label:first-child{margin-bottom:5px}body.post-type-tsml_meeting .meta_form_row textarea{height:140px}body.post-type-tsml_meeting .meta_form_row:last-child{margin-bottom:0}body.post-type-tsml_meeting .meta_form_row div.checkboxes{-moz-columns:3 auto;column-count:3;height:auto;overflow-x:hidden;overflow-y:auto;padding:0;position:relative}body.post-type-tsml_meeting .meta_form_row div.checkboxes label{-webkit-column-break-inside:avoid;box-sizing:border-box;display:block;float:none;font-size:13px;line-height:16px;margin:0;padding:5px 0 5px 20px;position:relative;text-align:left;width:100%}body.post-type-tsml_meeting .meta_form_row div.checkboxes label input{left:0;position:absolute;top:10px}body.post-type-tsml_meeting .meta_form_row div.checkboxes label.not_in_use{display:none}body.post-type-tsml_meeting .meta_form_row div.checkboxes .toggle_more{display:none;font-size:13px;position:absolute}body.post-type-tsml_meeting .meta_form_row div.checkboxes .toggle_more .less{display:none}body.post-type-tsml_meeting .meta_form_row div.checkboxes.has_more{padding-bottom:30px}body.post-type-tsml_meeting .meta_form_row div.checkboxes.has_more .toggle_more{bottom:0;display:block;left:0;position:absolute}body.post-type-tsml_meeting .meta_form_row div.checkboxes.showing_more label.not_in_use{display:block}body.post-type-tsml_meeting .meta_form_row div.checkboxes.showing_more .more{display:none}body.post-type-tsml_meeting .meta_form_row div.checkboxes.showing_more .less{display:block}body.post-type-tsml_meeting .meta_form_row.city input{margin-right:1%;width:60%}body.post-type-tsml_meeting .meta_form_row.city select{width:24%}body.post-type-tsml_meeting .meta_form_row ::-webkit-input-placeholder{color:#ccc}body.post-type-tsml_meeting .meta_form_row :-moz-placeholder,body.post-type-tsml_meeting .meta_form_row ::-moz-placeholder{color:#ccc}body.post-type-tsml_meeting .meta_form_row :-ms-input-placeholder{color:#ccc}body.post-type-tsml_meeting .meta_form_row #map{background-color:#f6f6f6;height:300px;padding:0}body.post-type-tsml_meeting .meta_form_row #map :focus{outline:none}body.post-type-tsml_meeting .meta_form_row #map .mapboxgl-ctrl-attrib,body.post-type-tsml_meeting .meta_form_row #map .mapboxgl-ctrl-logo{display:none}body.post-type-tsml_meeting .meta_form_row #map p{color:#999;font-size:15px;font-style:italic;margin:8px 10px}body.post-type-tsml_meeting .meta_form_row ol{-moz-column-gap:60px;column-gap:60px;-moz-columns:2 auto;column-count:2;font-size:15px;margin:0;overflow:auto;padding:10px 0 0}body.post-type-tsml_meeting .meta_form_row ol li{-moz-column-break-inside:avoid;-webkit-column-break-inside:avoid;break-inside:avoid-column;margin:0 0 0 25px;padding:0 0 10px 110px;position:relative}body.post-type-tsml_meeting .meta_form_row ol li span{color:#999;display:inline-block;left:0;position:absolute}body.post-type-tsml_meeting .meta_form_row ol li:last-child{margin-bottom:0}body.post-type-tsml_meeting .meta_form_row .container{display:table;float:left;width:85%}body.post-type-tsml_meeting .meta_form_row .container .row{display:table-row}body.post-type-tsml_meeting .meta_form_row .container .row:first-child div{border-top:0}body.post-type-tsml_meeting .meta_form_row .container .row div{border-left:10px solid transparent;border-top:10px solid transparent;display:table-cell}body.post-type-tsml_meeting .meta_form_row .container .row div:first-child{border-left:0}body.post-type-tsml_meeting .meta_form_row .container .row div input{width:100%}body.post-type-tsml_meeting #contact-type[data-type=meeting] .group-visible{display:none}body.post-type-tsml_meeting #contact-type[data-type=group] .group-visible{display:block}body.post-type-tsml_meeting div.in_person div{margin-bottom:5px}body.post-type-tsml_meeting div.location_note{border:1px solid #c3c4c7;border-left:4px solid #72aee6;margin-top:10px;padding:10px}body.post-type-tsml_meeting div.location_note ul{list-style-type:disc;margin:0 0 0 15px}body.post-type-tsml_meeting div.location_warning{border:1px solid #c3c4c7;border-left:4px solid #d3a73e;margin-top:5px;padding:10px}body.post-type-tsml_meeting div.location_error{border:1px solid #c3c4c7;border-left:4px solid #d40047;margin-top:5px;padding:10px}body .logo{display:block;float:right;margin-left:10px}body .logo img{display:block;height:auto;width:85px}
3 |
--------------------------------------------------------------------------------
/assets/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4recovery/12-step-meeting-list/90d53c92658a015c7bd6b4310e9a8e664386b4aa/assets/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/assets/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4recovery/12-step-meeting-list/90d53c92658a015c7bd6b4310e9a8e664386b4aa/assets/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4recovery/12-step-meeting-list/90d53c92658a015c7bd6b4310e9a8e664386b4aa/assets/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/assets/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4recovery/12-step-meeting-list/90d53c92658a015c7bd6b4310e9a8e664386b4aa/assets/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/assets/img/apple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
9 |
10 |
11 |
16 |
18 |
19 |
20 |
21 |
24 |
30 |
36 |
43 |
46 |
52 |
55 |
61 |
62 |
63 |
64 |
69 |
75 |
79 |
83 |
84 |
90 |
96 |
103 |
109 |
113 |
116 |
119 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/assets/img/code4recovery.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/img/ionicons-minus-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/assets/img/ionicons-minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/assets/img/ionicons-plus-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/assets/img/ionicons-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/assets/img/venmo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | image/svg+xml
71 |
--------------------------------------------------------------------------------
/assets/js/bootstrap.dropdown.js:
--------------------------------------------------------------------------------
1 | /* ========================================================================
2 | * Bootstrap: dropdown.js v3.3.7
3 | * http://getbootstrap.com/javascript/#dropdowns
4 | * ========================================================================
5 | * Copyright 2011-2016 Twitter, Inc.
6 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7 | * ======================================================================== */
8 |
9 |
10 | +function ($) {
11 | 'use strict';
12 |
13 | // DROPDOWN CLASS DEFINITION
14 | // =========================
15 |
16 | var backdrop = '.dropdown-backdrop'
17 | var toggle = '[data-toggle="tsml-dropdown"]'
18 | var Dropdown = function (element) {
19 | $(element).on('click.bs.dropdown', this.toggle)
20 | }
21 |
22 | Dropdown.VERSION = '3.3.7'
23 |
24 | function getParent($this) {
25 | var selector = $this.attr('data-target')
26 |
27 | if (!selector) {
28 | selector = $this.attr('href')
29 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
30 | }
31 |
32 | var $parent = selector && $(selector)
33 |
34 | return $parent && $parent.length ? $parent : $this.parent()
35 | }
36 |
37 | function clearMenus(e) {
38 | if (e && e.which === 3) return
39 | $(backdrop).remove()
40 | $(toggle).each(function () {
41 | var $this = $(this)
42 | var $parent = getParent($this)
43 | var relatedTarget = { relatedTarget: this }
44 |
45 | if (!$parent.hasClass('open')) return
46 |
47 | if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
48 |
49 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
50 |
51 | if (e.isDefaultPrevented()) return
52 |
53 | $this.attr('aria-expanded', 'false')
54 | $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
55 | })
56 | }
57 |
58 | Dropdown.prototype.toggle = function (e) {
59 | var $this = $(this)
60 |
61 | if ($this.is('.disabled, :disabled')) return
62 |
63 | var $parent = getParent($this)
64 | var isActive = $parent.hasClass('open')
65 |
66 | clearMenus()
67 |
68 | if (!isActive) {
69 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
70 | // if mobile we use a backdrop because click events don't delegate
71 | $(document.createElement('div'))
72 | .addClass('dropdown-backdrop')
73 | .insertAfter($(this))
74 | .on('click', clearMenus)
75 | }
76 |
77 | var relatedTarget = { relatedTarget: this }
78 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
79 |
80 | if (e.isDefaultPrevented()) return
81 |
82 | $this
83 | .trigger('focus')
84 | .attr('aria-expanded', 'true')
85 |
86 | $parent
87 | .toggleClass('open')
88 | .trigger($.Event('shown.bs.dropdown', relatedTarget))
89 | }
90 |
91 | return false
92 | }
93 |
94 | Dropdown.prototype.keydown = function (e) {
95 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
96 |
97 | var $this = $(this)
98 |
99 | e.preventDefault()
100 | e.stopPropagation()
101 |
102 | if ($this.is('.disabled, :disabled')) return
103 |
104 | var $parent = getParent($this)
105 | var isActive = $parent.hasClass('open')
106 |
107 | if (!isActive && e.which != 27 || isActive && e.which == 27) {
108 | if (e.which == 27) $parent.find(toggle).trigger('focus')
109 | return $this.trigger('click')
110 | }
111 |
112 | var desc = ' li:not(.disabled):visible a'
113 | var $items = $parent.find('.dropdown-menu' + desc)
114 |
115 | if (!$items.length) return
116 |
117 | var index = $items.index(e.target)
118 |
119 | if (e.which == 38 && index > 0) index-- // up
120 | if (e.which == 40 && index < $items.length - 1) index++ // down
121 | if (!~index) index = 0
122 |
123 | $items.eq(index).trigger('focus')
124 | }
125 |
126 |
127 | // DROPDOWN PLUGIN DEFINITION
128 | // ==========================
129 |
130 | function Plugin(option) {
131 | return this.each(function () {
132 | var $this = $(this)
133 | var data = $this.data('bs.dropdown')
134 |
135 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
136 | if (typeof option == 'string') data[option].call($this)
137 | })
138 | }
139 |
140 | var old = $.fn.dropdown
141 |
142 | $.fn.dropdown = Plugin
143 | $.fn.dropdown.Constructor = Dropdown
144 |
145 |
146 | // DROPDOWN NO CONFLICT
147 | // ====================
148 |
149 | $.fn.dropdown.noConflict = function () {
150 | $.fn.dropdown = old
151 | return this
152 | }
153 |
154 |
155 | // APPLY TO STANDARD DROPDOWN ELEMENTS
156 | // ===================================
157 |
158 | $(document)
159 | .on('click.bs.dropdown.data-api', clearMenus)
160 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
161 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
162 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
163 | .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
164 |
165 | }(jQuery);
166 |
--------------------------------------------------------------------------------
/assets/js/closestmeetings.min.js:
--------------------------------------------------------------------------------
1 | function closestmeetings() {
2 | navigator.geolocation.getCurrentPosition(show)
3 | }
4 |
5 | jQuery( document ).ready(function() {
6 | //console.log( "ready!" );
7 | closestmeetings();
8 | });
9 |
10 | function codeIt(arLoc) {
11 | return ''+arLoc['time_formatted']+' '+arLoc['name']+' '+arLoc['distance']+' mi ';
12 | }
13 |
14 | function show(position) {
15 |
16 | var geo = jQuery("#geolocate").val();
17 | //var geo = "any";
18 | jQuery.ajax({
19 | url : displayclosestmeetings.ajax_url,
20 | type : "GET",
21 | data: {
22 | action : 'display_closest_meetings',
23 | lat : position.coords.latitude,
24 | long:position.coords.longitude,
25 | today: geo
26 | },
27 | success : function( response ) {
28 |
29 | response.sort(function (a, b) {
30 | return a.time.localeCompare(b.time);
31 | });
32 |
33 | var txt = "";
34 |
35 | for (i=0; i * {
110 | flex: 1;
111 | }
112 | }
113 | }
114 |
115 | .stack {
116 | display: grid;
117 | gap: 20px;
118 | justify-items: flex-start;
119 | &.compact {
120 | gap: 8px;
121 | }
122 | }
123 |
124 | .wrap {
125 | margin: 0;
126 | padding: 20px 20px 0 0;
127 | }
128 |
129 | .row {
130 | display: flex;
131 | flex-wrap: wrap;
132 | gap: 9px;
133 | }
134 |
135 | .postbox {
136 | margin: 0;
137 | padding: 20px;
138 | width: 100%;
139 | box-sizing: border-box;
140 |
141 | * {
142 | margin: 0;
143 | }
144 |
145 | .logo {
146 | margin-top: -30px;
147 | }
148 |
149 | details {
150 | summary {
151 | cursor: pointer;
152 | font-size: 1rem;
153 | font-weight: bold;
154 | }
155 | p {
156 | margin: 10px 0 0;
157 | }
158 | }
159 |
160 | ul {
161 | margin: 10px 0 0 20px;
162 | li + li {
163 | margin-top: 10px;
164 | }
165 | &.types {
166 | column-gap: 40px;
167 | columns: 2 auto;
168 | margin: 20px 0;
169 | line-height: 1.4;
170 | }
171 | }
172 |
173 | select {
174 | width: 100%;
175 | }
176 |
177 | //radio import options
178 | form {
179 | input {
180 | box-shadow: none;
181 |
182 | &[type='text'],
183 | &[type='email'],
184 | &[type='url'] {
185 | font-family: monospace;
186 | width: 300px;
187 | }
188 |
189 | &[type='submit'] {
190 | min-width: 80px;
191 | }
192 | }
193 |
194 | select {
195 | width: 300px;
196 | }
197 |
198 | &.radio label {
199 | display: flex;
200 | justify-content: flex-start;
201 | gap: 0.25rem;
202 | input[type='radio'] {
203 | margin-top: 3px;
204 | }
205 | }
206 | }
207 |
208 | //get feedback & get notified
209 | table {
210 | margin: 0 0 10px;
211 | width: 100%;
212 | border: 0;
213 | border-spacing: 0;
214 | border-collapse: collapse;
215 |
216 | tr {
217 | th,
218 | td {
219 | text-align: left;
220 | margin: 0;
221 | padding: 4px 0;
222 | border: 0;
223 |
224 | input.button {
225 | margin-right: 10px;
226 | }
227 |
228 | &.align-center {
229 | text-align: center;
230 | }
231 |
232 | &.align-right {
233 | text-align: right;
234 | }
235 |
236 | &.small {
237 | width: 1%;
238 | }
239 |
240 | &:first-child {
241 | font-family: monospace;
242 | }
243 |
244 | &:last-child {
245 | text-align: right;
246 |
247 | span {
248 | cursor: pointer;
249 | }
250 | }
251 | }
252 |
253 | td {
254 | border-top: 1px solid #ddd;
255 | }
256 |
257 | &:last-child td {
258 | padding-bottom: 0;
259 | }
260 | }
261 |
262 | form {
263 | .button.button-small {
264 | height: 18px;
265 | line-height: 16px;
266 | }
267 | }
268 | }
269 |
270 | //try the apps app store buttons
271 | try_the_apps {
272 | p.buttons {
273 | margin-left: -5px;
274 | margin-right: -5px;
275 | overflow: auto;
276 |
277 | a {
278 | padding: 0 5px;
279 | width: 50%;
280 | float: left;
281 | display: block;
282 | box-sizing: border-box;
283 |
284 | img {
285 | width: 100%;
286 | height: auto;
287 | display: block;
288 | }
289 | }
290 | }
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/assets/src/admin-meeting.scss:
--------------------------------------------------------------------------------
1 | div.notice ul {
2 | list-style: disc;
3 | padding-left: 1rem;
4 | }
5 |
6 | .postbox {
7 | .handlediv {
8 | display: none;
9 | }
10 |
11 | .hndle {
12 | background-color: #fafafa;
13 | }
14 |
15 | &.closed .inside {
16 | display: block;
17 | }
18 |
19 | //group Information (optional) gray
20 | h2 small,
21 | h3 small {
22 | color: #aaa;
23 | font-size: inherit;
24 |
25 | &:before {
26 | content: ' (';
27 | }
28 |
29 | &:after {
30 | content: ')';
31 | }
32 | }
33 | }
34 |
35 | .meta_form_separator {
36 | margin-left: 15%;
37 |
38 | h4 {
39 | border-bottom: 1px solid #ccc;
40 | margin-bottom: 0;
41 | padding-bottom: 10px;
42 | }
43 |
44 | p {
45 | margin: 10px 0 0;
46 | }
47 | }
48 |
49 | .meta_form_row {
50 | padding: 10px 0 0;
51 | overflow: auto;
52 | clear: left;
53 |
54 | label,
55 | input,
56 | div.checkboxes,
57 | #map {
58 | float: left;
59 | line-height: 100%;
60 | }
61 |
62 | label {
63 | width: 13%;
64 | margin-right: 2%;
65 | text-align: right;
66 | font-size: 15px;
67 | line-height: 1.4;
68 | margin-top: 9px;
69 | white-space: nowrap;
70 | }
71 |
72 | input[type='text'],
73 | input[type='date'],
74 | input[type='email'],
75 | input[type='url'],
76 | select,
77 | textarea,
78 | div.checkboxes,
79 | #map {
80 | width: 85%;
81 | margin: 0;
82 | font-size: 18px;
83 | padding: 6px;
84 | height: 40px;
85 | line-height: normal;
86 | float: left;
87 | box-sizing: border-box;
88 | }
89 |
90 | input[type='text'],
91 | input[type='date'],
92 | select,
93 | textarea,
94 | #map {
95 | border: 1px solid #ddd !important;
96 | border-radius: 4px !important;
97 | }
98 |
99 | input.time {
100 | width: 42%;
101 |
102 | &:last-child {
103 | margin-left: 1%;
104 | }
105 | }
106 |
107 | small {
108 | display: none;
109 | margin-left: 15%;
110 | margin-top: 0.25rem;
111 | font-size: 0.8rem;
112 | &.error_message{
113 | color: #d40047;
114 | }
115 | &.warning_message{
116 | color: #a14c00;
117 | }
118 | &.show {
119 | display: inline-block;
120 | }
121 | }
122 |
123 | input,
124 | select {
125 | border: 1px solid #ccc;
126 | }
127 |
128 | input[type='radio'],
129 | input[type='checkbox'] {
130 | box-shadow: none;
131 |
132 | &:checked:before {
133 | display: none;
134 | }
135 | }
136 | //wordpress is doing something fancy with checkboxes and it's not working
137 | input[type='checkbox'] {
138 | -webkit-appearance: checkbox;
139 | }
140 |
141 | input[type='radio'] {
142 | -webkit-appearance: radio;
143 | }
144 |
145 | input[disabled] {
146 | opacity: 0.5;
147 | cursor: not-allowed;
148 | }
149 |
150 | input.error {
151 | border: solid 1px #d40047 !important;
152 | }
153 |
154 | input.warning {
155 | border: solid 1px #d3a73e !important;
156 | }
157 |
158 | &.checkbox,
159 | &.radio {
160 | padding-left: 15%;
161 | line-height: 1;
162 |
163 | label {
164 | width: auto;
165 | margin: 0;
166 | font-size: 13px;
167 | display: block;
168 | float: none;
169 | text-align: left;
170 | position: relative;
171 | padding: 0 0 0 20px;
172 |
173 | input {
174 | position: absolute;
175 | left: 0;
176 | top: 5px;
177 | }
178 |
179 | &:first-child {
180 | margin-bottom: 5px;
181 | }
182 | }
183 | }
184 |
185 | textarea {
186 | height: 140px;
187 | }
188 |
189 | &:last-child {
190 | margin-bottom: 0;
191 | }
192 |
193 | div.checkboxes {
194 | overflow-x: hidden;
195 | overflow-y: auto;
196 | padding: 0;
197 | height: auto;
198 | -webkit-columns: 3 auto;
199 | -moz-columns: 3 auto;
200 | columns: 3 auto;
201 | position: relative;
202 |
203 | label {
204 | float: none;
205 | display: block;
206 | margin: 0;
207 | line-height: 16px;
208 | width: 100%;
209 | text-align: left;
210 | font-size: 13px;
211 | -webkit-column-break-inside: avoid;
212 | padding: 5px 0 5px 20px;
213 | position: relative;
214 | box-sizing: border-box;
215 |
216 | input {
217 | position: absolute;
218 | top: 10px;
219 | left: 0;
220 | }
221 |
222 | &.not_in_use {
223 | display: none;
224 | }
225 | }
226 |
227 | .toggle_more {
228 | display: none;
229 | position: absolute;
230 | font-size: 13px;
231 |
232 | .less {
233 | display: none;
234 | }
235 | }
236 |
237 | &.has_more {
238 | padding-bottom: 30px;
239 |
240 | .toggle_more {
241 | position: absolute;
242 | bottom: 0;
243 | display: block;
244 | left: 0;
245 | }
246 | }
247 |
248 | &.showing_more {
249 | label.not_in_use {
250 | display: block;
251 | }
252 |
253 | .more {
254 | display: none;
255 | }
256 |
257 | .less {
258 | display: block;
259 | }
260 | }
261 | }
262 |
263 | &.city {
264 | input {
265 | width: 60%;
266 | margin-right: 1%;
267 | }
268 |
269 | select {
270 | width: 24%;
271 | }
272 | }
273 |
274 | ::-webkit-input-placeholder {
275 | color: #ccc;
276 | }
277 |
278 | :-moz-placeholder {
279 | color: #ccc;
280 | }
281 |
282 | ::-moz-placeholder {
283 | color: #ccc;
284 | }
285 |
286 | :-ms-input-placeholder {
287 | color: #ccc;
288 | }
289 |
290 | #map {
291 | background-color: #f6f6f6;
292 | height: 300px;
293 | padding: 0;
294 |
295 | *:focus {
296 | outline: none;
297 | }
298 |
299 | .mapboxgl-ctrl-logo,
300 | .mapboxgl-ctrl-attrib {
301 | display: none;
302 | }
303 |
304 | p {
305 | font-style: italic;
306 | font-size: 15px;
307 | margin: 8px 10px;
308 | color: #999;
309 | }
310 | }
311 | //meetings at a location
312 | ol {
313 | column-gap: 60px;
314 | columns: 2 auto;
315 | font-size: 15px;
316 | margin: 0;
317 | overflow: auto;
318 | padding: 10px 0 0 0;
319 |
320 | li {
321 | break-inside: avoid-column;
322 | -webkit-column-break-inside: avoid;
323 | padding: 0 0 10px 110px;
324 | position: relative;
325 | margin: 0 0 0 25px;
326 |
327 | span {
328 | color: #999;
329 | display: inline-block;
330 | position: absolute;
331 | left: 0;
332 | }
333 |
334 | &:last-child {
335 | margin-bottom: 0;
336 | }
337 | }
338 | }
339 |
340 | .container {
341 | width: 85%;
342 | float: left;
343 | display: table;
344 |
345 | .row {
346 | display: table-row;
347 |
348 | &:first-child div {
349 | border-top: 0;
350 | }
351 |
352 | div {
353 | border-left: 10px solid transparent;
354 | border-top: 10px solid transparent;
355 | display: table-cell;
356 |
357 | &:first-child {
358 | border-left: 0;
359 | }
360 |
361 | input {
362 | width: 100%;
363 | }
364 | }
365 | }
366 | }
367 | }
368 |
369 | #contact-type {
370 | &[data-type='meeting'] .group-visible {
371 | display: none;
372 | }
373 |
374 | &[data-type='group'] .group-visible {
375 | display: block;
376 | }
377 | }
378 |
379 | // Note box for in-person question
380 | div.in_person div {
381 | margin-bottom: 5px;
382 | }
383 |
384 | div.location_note {
385 | padding: 10px;
386 | border: solid 1px #c3c4c7;
387 | border-left: solid 4px #72aee6;
388 | margin-top: 10px;
389 | }
390 |
391 | div.location_note ul {
392 | list-style-type: disc;
393 | margin: 0 0 0 15px;
394 | }
395 |
396 | div.location_warning {
397 | padding: 10px;
398 | border: solid 1px #c3c4c7;
399 | border-left: solid 4px #d3a73e;
400 | margin-top: 5px;
401 | }
402 |
403 | div.location_error {
404 | padding: 10px;
405 | border: solid 1px #c3c4c7;
406 | border-left: solid 4px #d40047;
407 | margin-top: 5px;
408 | }
409 |
--------------------------------------------------------------------------------
/assets/src/admin.scss:
--------------------------------------------------------------------------------
1 | @import 'tsml-jquery-ui.css';
2 |
3 | body {
4 | //import & settings
5 | .wrap.tsml_admin_settings {
6 | @import 'admin-import-settings.scss';
7 | }
8 |
9 | //region list page
10 | &.taxonomy-tsml_region {
11 | .row-actions {
12 | display: none;
13 | }
14 | }
15 | //meeting add/edit
16 | &.post-type-tsml_meeting {
17 | @import 'admin-meeting.scss';
18 | }
19 |
20 | .logo {
21 | display: block;
22 | float: right;
23 | margin-left: 10px;
24 | img {
25 | display: block;
26 | height: auto;
27 | width: 85px;
28 | }
29 | }
30 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/assets/src/blocks/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schemas.wp.org/trunk/block.json",
3 | "apiVersion": 2,
4 | "name": "tsml/meetings",
5 | "title": "Meetings",
6 | "description": "12 Step Meeting List Meeting Block",
7 | "category": "embed",
8 | "icon": "groups",
9 | "attributes": {
10 | "alertBackgroundColor": { "type": "string", "default": "#faf4e0" },
11 | "alertTextColor": { "type": "string", "default": "#998a5e" },
12 | "backgroundColor": { "type": "string", "default": "#fff" },
13 | "borderRadius": { "type": "number", "default": 4 },
14 | "focusColor": { "type": "string", "default": "#0d6efd40" },
15 | "fontFamily": { "type": "string", "default": "system-ui, -apple-system, sans-serif" },
16 | "inPersonBadgeColor": { "type": "string", "default": "#146c43" },
17 | "inactiveBadgeColor": { "type": "string", "default": "#b02a37" },
18 | "linkColor": { "type": "string", "default": "#0d6efd" },
19 | "onlineBadgeColor": { "type": "string", "default": "#0a58ca" },
20 | "onlineBackgroundImage": {
21 | "type": "string",
22 | "default": "url(https://images.unsplash.com/photo-1588196749597-9ff075ee6b5b?...)"
23 | },
24 | "textColor": { "type": "string", "default": "#212529" }
25 | },
26 | "supports": {
27 | "html": false,
28 | "align": true,
29 | "alignWide": true,
30 | "anchor": true,
31 | "className": true,
32 | "spacing": {
33 | "margin": true,
34 | "padding": false,
35 | "blockGap": false
36 | },
37 | "typography": {
38 | "fontSize": true,
39 | "__experimentalFontFamily": true,
40 | "__experimentalFontStyle": true,
41 | "__experimentalFontWeight": true,
42 | "__experimentalTextTransform": true
43 | }
44 | },
45 | "textdomain": "12-step-meeting-list",
46 | "render": "file:./render.php",
47 | "editorScript": "file:./meetings.js"
48 | }
49 |
--------------------------------------------------------------------------------
/assets/src/blocks/edit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Renders the edit interface for the Meeting block
3 | */
4 | import {BlockIcon, InspectorControls, MediaUpload, useBlockProps} from '@wordpress/block-editor';
5 | import {
6 | __experimentalDivider as Divider,
7 | Button,
8 | ColorPalette,
9 | PanelBody,
10 | Placeholder,
11 | RangeControl,
12 | } from '@wordpress/components';
13 | import {useSelect} from '@wordpress/data';
14 | import {__} from '@wordpress/i18n';
15 |
16 | /**
17 | * Edit component for customizing styles and attributes of the block
18 | * @param attributes
19 | * @param setAttributes
20 | * @returns {JSX.Element}
21 | * @constructor
22 | */
23 | const Edit = ({attributes, setAttributes}) => {
24 | const {
25 | alertBackgroundColor,
26 | alertTextColor,
27 | backgroundColor,
28 | borderRadius,
29 | focusColor,
30 | fontFamily,
31 | fontSize,
32 | inPersonBadgeColor,
33 | inactiveBadgeColor,
34 | linkColor,
35 | onlineBadgeColor,
36 | onlineBackgroundImage,
37 | textColor
38 | } = attributes;
39 | const colorPalette = useSelect('core/block-editor').getSettings().colors
40 | const blockProps = useBlockProps({
41 | style: {
42 | '--alert-background': alertBackgroundColor,
43 | '--alert-text': alertTextColor,
44 | '--background': backgroundColor,
45 | '--border-radius': borderRadius + 'px',
46 | '--focus': focusColor,
47 | '--font-family': fontFamily,
48 | '--font-size': fontSize + 'px',
49 | '--in-person': inPersonBadgeColor,
50 | '--inactive': inactiveBadgeColor,
51 | '--link': linkColor,
52 | '--online': onlineBadgeColor,
53 | '--online-background-image': `url(${onlineBackgroundImage})`,
54 | '--text': textColor
55 | }
56 | })
57 | const {serverSideRender: ServerSideRender} = wp
58 | const legendStyles = {
59 | fontSize: '11px',
60 | fontWeight: '500',
61 | lineHeight: '1.4',
62 | textTransform: 'uppercase',
63 | display: 'block',
64 | padding: '0',
65 | marginBottom: '1.5em'
66 | }
67 | const helpStyles = {
68 | marginTop: '-.5em',
69 | marginBottom: '1.5em',
70 | fontSize: '11px',
71 | }
72 | return (
73 |
74 |
75 |
76 |
77 |
Background color
78 |
79 | Applies to entire meeting list block.
80 |
81 |
setAttributes({backgroundColor: value})}
85 | />
86 |
87 |
88 |
89 |
Online Background image
90 |
91 | Will be shown instead of a map for online meetings (approx 2000px x 2000px).
92 |
93 |
setAttributes({onlineBackgroundImage: media.url})}
95 | allowedTypes={['image']}
96 | value={onlineBackgroundImage}
97 | render={({open}) => (
98 |
99 | {__('Select Background Image', '12-step-meeting-list')}
100 |
101 | )}
102 | />
103 | {onlineBackgroundImage && (
104 | setAttributes({onlineBackgroundImage: ''})}
106 | variant="secondary"
107 | style={{marginTop: '10px', marginBottom: '1.5em'}}
108 | >
109 | {__('Remove Background Image', '12-step-meeting-list')}
110 |
111 | )}
112 |
113 |
114 |
115 |
116 | Text color
117 | setAttributes({textColor: value})} />
118 |
119 |
120 |
121 | Link color
122 | setAttributes({linkColor: value})} />
123 |
124 |
125 |
126 | Input focus shadow color
127 | setAttributes({focusColor: value})}
132 | />
133 |
134 |
135 |
136 |
137 | Background color
138 | setAttributes({alertBackgroundColor: value})}
142 | />
143 |
144 |
145 |
146 | Text color
147 | setAttributes({alertTextColor: value})} />
148 |
149 |
150 |
151 |
152 | In person meeting
153 | setAttributes({inPersonBadgeColor: value})}
157 | />
158 |
159 |
160 |
161 | Inactive meeting
162 | setAttributes({inactiveBadgeColor: value})}
166 | />
167 |
168 |
169 |
170 | Online meeting
171 | setAttributes({onlineBadgeColor: value})}
176 | />
177 |
178 |
179 |
180 |
181 | setAttributes({borderRadius: newValue})}
185 | min={0}
186 | max={50}
187 | />
188 |
189 |
190 |
191 |
}
193 | label={__('Meetings', '12-step-meeting-list')}
194 | instructions={__(
195 | "View the page to see the block. it's recommended not to put any page content below the block, and to make the block as wide as possible.",
196 | '12-step-meeting-list'
197 | )}
198 | >
199 |
200 |
201 | );
202 | };
203 | export default Edit;
204 |
--------------------------------------------------------------------------------
/assets/src/blocks/meetings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Registers the block provided a unique name and an object defining its behavior.
3 | *
4 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
5 | */
6 | import {registerBlockType} from '@wordpress/blocks';
7 |
8 | /**
9 | * Internal dependencies
10 | */
11 | import json from './block.json';
12 | import Edit from './edit';
13 |
14 | /**
15 | * Registering the new block type definition.
16 | *
17 | */
18 | registerBlockType(json, {
19 | edit: Edit
20 | });
21 |
--------------------------------------------------------------------------------
/assets/src/blocks/render.php:
--------------------------------------------------------------------------------
1 | $attributes['backgroundColor'] ?? null,
28 | '--alert-background' => $attributes['alertBackgroundColor'] ?? null,
29 | '--alert-text' => $attributes['alertTextColor'] ?? null,
30 | '--in-person' => $attributes['inPersonBadgeColor'] ?? null,
31 | '--inactive' => $attributes['inactiveBadgeColor'] ?? null,
32 | '--link' => $attributes['linkColor'] ?? null,
33 | '--online' => $attributes['onlineBadgeColor'] ?? null,
34 | '--text' => $attributes['textColor'] ?? null,
35 | '--focus' => $attributes['focusColor'] ?? null,
36 | '--border-radius' => isset($attributes['borderRadius']) ? $attributes['borderRadius'].'px' : null,
37 | '--font-family' => isset($attributes['fontFamily']) ? 'var(--wp--preset--font-family--'.$attributes['fontFamily'].')' : null,
38 | '--online-background-image' => isset($attributes['onlineBackgroundImage']) ? 'url('.$attributes['onlineBackgroundImage'].')' : null,
39 | "--font-size" => $size ?? null,
40 | ];
41 |
42 | /** Load TSML assets */
43 | tsml_assets();
44 |
45 | /** Loop through styles & output inline ';
53 | ?>
54 |
55 | >
56 |
57 |
58 |
--------------------------------------------------------------------------------
/assets/src/maps.js:
--------------------------------------------------------------------------------
1 | //map functions -- methods must all support both google maps and mapbox
2 |
3 | //declare some global variables
4 | var infowindow,
5 | searchLocation,
6 | searchMarker,
7 | tsmlmap,
8 | markers = [],
9 | bounds,
10 | mapMode = 'none',
11 | locationIcon,
12 | searchIcon;
13 |
14 | //create an empty map
15 | function createMap(scrollwheel, locations, searchLocation) {
16 | if (tsml.debug) console.log('createMap() locations', locations);
17 | if (tsml.mapbox_key) {
18 | mapMode = 'mapbox';
19 |
20 | mapboxgl.accessToken = tsml.mapbox_key;
21 |
22 | //init map
23 | if (!tsmlmap) {
24 | tsmlmap = new mapboxgl.Map({
25 | container: 'map',
26 | style: tsml.mapbox_theme || 'mapbox://styles/mapbox/streets-v9'
27 | });
28 |
29 | //add zoom control
30 | tsmlmap.addControl(
31 | new mapboxgl.NavigationControl({
32 | showCompass: false
33 | })
34 | );
35 | }
36 |
37 | //init bounds
38 | bounds = {
39 | north: false,
40 | south: false,
41 | east: false,
42 | west: false
43 | };
44 |
45 | //custom marker icons
46 | locationIcon = window.btoa(
47 | ' '
48 | );
49 | searchIcon = window.btoa(
50 | ' '
51 | );
52 | } else if (tsml.google_maps_key) {
53 | //check to see if google ready (wp google maps was removing other map scripts for a while)
54 | if (typeof google !== 'object') {
55 | console.warn('google key present but google script not ready');
56 | return;
57 | }
58 |
59 | mapMode = 'google';
60 |
61 | //init map
62 | if (!tsmlmap)
63 | tsmlmap = new google.maps.Map(document.getElementById('map'), {
64 | disableDefaultUI: true,
65 | scrollwheel: scrollwheel,
66 | zoomControl: true
67 | });
68 |
69 | //init popup
70 | infowindow = new google.maps.InfoWindow();
71 |
72 | //init bounds
73 | bounds = new google.maps.LatLngBounds();
74 | }
75 |
76 | setMapMarkers(locations, searchLocation);
77 | }
78 |
79 | //format an address: replace commas with breaks
80 | function formatAddress(address, street_only) {
81 | if (!address) return '';
82 | address = address.split(', ');
83 | if (street_only) return address[0];
84 | if (address[address.length - 1] == 'USA') {
85 | address.pop(); //don't show USA
86 | var state_and_zip = address.pop();
87 | address[address.length - 1] += ', ' + state_and_zip;
88 | }
89 | return address.join(' ');
90 | }
91 |
92 | //format a link to a meeting result page, preserving all but the excluded query string keys
93 | function formatLink(url, text, exclude) {
94 | if (!url) return text;
95 | if (location.search) {
96 | var query_pairs = location.search.substr(1).split('&');
97 | var new_query_pairs = [];
98 | for (var i = 0; i < query_pairs.length; i++) {
99 | var query_parts = query_pairs[i].split('=');
100 | if (query_parts[0] != exclude) new_query_pairs[new_query_pairs.length] = query_parts[0] + '=' + query_parts[1];
101 | }
102 | if (new_query_pairs.length) {
103 | url += (url.indexOf('?') == -1 ? '?' : '&') + new_query_pairs.join('&');
104 | }
105 | }
106 | return '' + text + ' ';
107 | }
108 |
109 | //remove search marker
110 | function removeSearchMarker() {
111 | searchLocation = null;
112 | if (typeof searchMarker == 'object' && searchMarker) {
113 | searchMarker.setMap(null);
114 | searchMarker = null;
115 | }
116 | }
117 |
118 | //set / initialize map
119 | function setMapBounds() {
120 | if (mapMode == 'google') {
121 | if (markers.length > 1) {
122 | //multiple markers
123 | tsmlmap.fitBounds(bounds);
124 | } else if (markers.length == 1) {
125 | //if only one marker, zoom in and click the infowindow
126 | var center = bounds.getCenter();
127 | if (!center) return;
128 | if (markers[0].getClickable()) {
129 | tsmlmap.setCenter({lat: center.lat() + 0.0025, lng: center.lng()});
130 | google.maps.event.trigger(markers[0], 'click');
131 | } else {
132 | tsmlmap.setCenter({lat: center.lat(), lng: center.lng()});
133 | }
134 | tsmlmap.setZoom(15);
135 | }
136 | } else if (mapMode == 'mapbox') {
137 | if (markers.length > 1) {
138 | //multiple markers
139 | tsmlmap.fitBounds(
140 | [
141 | [bounds.west, bounds.south],
142 | [bounds.east, bounds.north]
143 | ],
144 | {
145 | duration: 0,
146 | padding: 100
147 | }
148 | );
149 | } else if (markers.length == 1) {
150 | //if only one marker, zoom in and open the popup if it exists
151 | if (markers[0].getPopup()) {
152 | tsmlmap.setZoom(14).setCenter([bounds.east, bounds.north + 0.0025]);
153 | markers[0].togglePopup();
154 | } else {
155 | tsmlmap.setZoom(14).setCenter([bounds.east, bounds.north]);
156 | }
157 | }
158 | }
159 | }
160 |
161 | //set single marker, called by all public pages
162 | function setMapMarker(title, position, content) {
163 | //stop if coordinates are empty
164 | if (!position.lat && !position.lng) return;
165 |
166 | var marker;
167 |
168 | if (mapMode == 'google') {
169 | //set new marker
170 | marker = new google.maps.Marker({
171 | position: position,
172 | map: tsmlmap,
173 | title: title,
174 | icon: {
175 | path: 'M20.5,0.5 c11.046,0,20,8.656,20,19.333c0,10.677-12.059,21.939-20,38.667c-5.619-14.433-20-27.989-20-38.667C0.5,9.156,9.454,0.5,20.5,0.5z',
176 | fillColor: '#f76458',
177 | fillOpacity: 1,
178 | anchor: new google.maps.Point(40, 50),
179 | strokeWeight: 2,
180 | strokeColor: '#b3382c',
181 | scale: 0.6
182 | }
183 | });
184 |
185 | //add infowindow event
186 | if (content) {
187 | google.maps.event.addListener(
188 | marker,
189 | 'click',
190 | (function (marker) {
191 | return function () {
192 | infowindow.setContent('' + content + '
');
193 | infowindow.open(tsmlmap, marker);
194 | };
195 | })(marker)
196 | );
197 | } else {
198 | marker.setClickable(false); //we'll check this when setting center
199 | }
200 | } else if (mapMode == 'mapbox') {
201 | var el = document.createElement('div');
202 | el.className = 'marker';
203 | el.style.backgroundImage = 'url(data:image/svg+xml;base64,' + locationIcon + ')';
204 | el.style.width = '26px';
205 | el.style.height = '38.4px';
206 |
207 | marker = new mapboxgl.Marker(el).setLngLat(position);
208 |
209 | if (content) {
210 | var popup = new mapboxgl.Popup({offset: 25});
211 | popup.setHTML(content);
212 | marker.setPopup(popup);
213 | }
214 |
215 | marker.addTo(tsmlmap);
216 | }
217 |
218 | return marker;
219 | }
220 |
221 | //add one or more markers to a map
222 | function setMapMarkers(locations, searchLocation) {
223 | //remove existing markers
224 | if (markers.length) {
225 | for (var i = 0; i < markers.length; i++) {
226 | if (mapMode == 'google') {
227 | markers[i].setMap(null);
228 | } else if (mapMode == 'mapbox') {
229 | markers[i].remove();
230 | }
231 | }
232 | markers = [];
233 | }
234 |
235 | //set search location?
236 | removeSearchMarker();
237 | if (searchLocation) {
238 | if (tsml.debug) console.log('setMapMarker() searchLocation', searchLocation);
239 | setSearchMarker(searchLocation);
240 | }
241 |
242 | //convert to array and sort it by latitude (for marker overlaps)
243 | var location_array =
244 | typeof locations === 'object'
245 | ? Object.keys(locations)
246 | .map(function (e) {
247 | return locations[e];
248 | })
249 | .sort(function (a, b) {
250 | return b.latitude - a.latitude;
251 | })
252 | : [];
253 |
254 | //loop through and create new markers
255 | for (var i = 0; i < location_array.length; i++) {
256 | var location = location_array[i];
257 | if (tsml.debug) console.log('setMapMarkers() location', location);
258 | var content;
259 |
260 | if (location.url && location.formatted_address && !location.approximate) {
261 | //create infowindow content
262 | content =
263 | '' +
264 | formatLink(location.url, location.name, 'post_type') +
265 | ' ' +
266 | '' +
267 | formatAddress(location.formatted_address) +
268 | ' ';
269 |
270 | //make directions button
271 | if (location.directions && location.directions_url) {
272 | content +=
273 | '' +
276 | '' +
277 | ' ' +
278 | ' ' +
279 | ' ' +
280 | location.directions +
281 | ' ';
282 | }
283 |
284 | //make meeting list
285 | if (location.meetings && location.meetings.length) {
286 | var current_day = null;
287 | for (var j = 0; j < location.meetings.length; j++) {
288 | var meeting = location.meetings[j];
289 | if (current_day != meeting.day) {
290 | if (current_day) content += '';
291 | current_day = meeting.day;
292 | if (typeof tsml.days[current_day] !== 'undefined') content += '' + tsml.days[current_day] + ' ';
293 | content += '';
294 | }
295 | content += '' + meeting.time + ' ' + formatLink(meeting.url, meeting.name, 'post_type') + ' ';
296 | }
297 | content += ' ';
298 | }
299 | }
300 |
301 | //make coordinates numeric
302 | var position = {
303 | lat: parseFloat(location.latitude),
304 | lng: parseFloat(location.longitude)
305 | };
306 |
307 | var marker = setMapMarker(location.name, position, content);
308 |
309 | //manage bounds and set "visibility" if not approximate location
310 | if (typeof marker == 'object' && marker) {
311 | if (mapMode == 'google') {
312 | bounds.extend(marker.position);
313 | if (location.approximate === 'yes') marker.setVisible(false);
314 | } else if (mapMode == 'mapbox') {
315 | if (!bounds.north || position.lat > bounds.north) bounds.north = position.lat;
316 | if (!bounds.south || position.lat < bounds.south) bounds.south = position.lat;
317 | if (!bounds.east || position.lng > bounds.east) bounds.east = position.lng;
318 | if (!bounds.west || position.lng < bounds.west) bounds.west = position.lng;
319 | if (location.approximate === 'yes') marker.remove();
320 | }
321 | }
322 |
323 | if (tsml.debug) console.log('setMapMarkers() marker', marker);
324 |
325 | markers.push(marker);
326 | }
327 |
328 | setMapBounds();
329 | }
330 |
331 | //set or remove the search marker (user location or search center)
332 | function setSearchMarker(data) {
333 | removeSearchMarker();
334 | if (!data || !data.latitude) return;
335 | if (mapMode == 'google') {
336 | searchMarker = new google.maps.Marker({
337 | icon: {
338 | path: 'M20.5,0.5 c11.046,0,20,8.656,20,19.333c0,10.677-12.059,21.939-20,38.667c-5.619-14.433-20-27.989-20-38.667C0.5,9.156,9.454,0.5,20.5,0.5z',
339 | fillColor: '#2c78b3',
340 | fillOpacity: 1,
341 | anchor: new google.maps.Point(40, 50),
342 | strokeWeight: 2,
343 | strokeColor: '#2c52b3',
344 | scale: 0.6
345 | },
346 | position: new google.maps.LatLng(data.latitude, data.longitude),
347 | map: tsmlmap
348 | });
349 |
350 | bounds.extend(searchMarker.position);
351 | } else if (mapMode == 'mapbox') {
352 | var el = document.createElement('div');
353 | el.className = 'marker';
354 | el.style.backgroundImage = 'url(data:image/svg+xml;base64,' + searchIcon + ')';
355 | el.style.width = '26px';
356 | el.style.height = '38.4px';
357 |
358 | marker = new mapboxgl.Marker(el).setLngLat([data.longitude, data.latitude]).addTo(tsmlmap);
359 | }
360 | }
361 |
--------------------------------------------------------------------------------
/assets/src/mixins/breakpoints.scss:
--------------------------------------------------------------------------------
1 | @mixin breakpoint-768() {
2 | @media screen and (min-width: 768px) {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin breakpoint-992() {
8 | @media screen and (min-width: 992px) {
9 | @content;
10 | }
11 | }
12 |
13 | @mixin breakpoint-1200() {
14 | @media screen and (min-width: 1200px) {
15 | @content;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/assets/src/tsml-jquery-ui.css:
--------------------------------------------------------------------------------
1 | .ui-autocomplete {
2 | min-width: 270px;
3 | font-family: inherit;
4 | font-size: 0.8em;
5 | border-radius: 4px;
6 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
7 | }
8 |
9 | .ui-autocomplete-category {
10 | border-bottom: 1px solid #ccc;
11 | font-size: 0.8em;
12 | font-weight: bold;
13 | margin: 10px 0px 0px 0px;
14 | padding: 6px 20px;
15 | text-transform: uppercase;
16 | }
17 |
18 | .ui-autocomplete-highlight {
19 | font-weight: bold !important;
20 | }
21 |
22 | .ui-menu-item {
23 | padding: 6px 20px !important;
24 | }
25 |
--------------------------------------------------------------------------------
/includes/admin_lists.php:
--------------------------------------------------------------------------------
1 | 'Region',
13 | 'orderby' => 'tax_name',
14 | 'selected' => !empty($_GET['region']) ? sanitize_text_field($_GET['region']) : '',
15 | 'hierarchical' => true,
16 | 'name' => 'region',
17 | 'taxonomy' => 'tsml_region',
18 | 'hide_if_empty' => true,
19 | ]);
20 |
21 | $types = [];
22 | foreach ($tsml_types_in_use as $type) {
23 | $types[$type] = $tsml_programs[$tsml_program]['types'][$type];
24 | }
25 | asort($types);
26 |
27 | echo '';
28 | echo '' . esc_html__('Type', '12-step-meeting-list') . ' ';
29 | foreach ($types as $key => $value) {
30 | echo '' . esc_html($value) . ' ';
31 | }
32 | echo ' ';
33 |
34 | $data_sources = [
35 | -1 => __('None', '12-step-meeting-list')
36 | ];
37 | foreach (array_values($tsml_data_sources) as $index => $data_source) {
38 | $data_sources[$index] = $data_source['name'];
39 | }
40 |
41 | echo '';
42 | echo '' . esc_html__('Data Source', '12-step-meeting-list') . ' ';
43 | foreach ($data_sources as $key => $value) {
44 | echo '' . esc_html($value) . ' ';
45 | }
46 | echo ' ';
47 |
48 | }, 10, 1);
49 |
50 | // if filter is set, restrict results
51 | add_filter(
52 | 'pre_get_posts',
53 | function ($query) {
54 | global $post_type, $pagenow, $wpdb, $tsml_data_sources;
55 |
56 | if ($pagenow === 'edit.php' && $post_type === 'tsml_meeting' && $query->is_main_query()) {
57 |
58 | $meta_query = [];
59 |
60 | if (!empty($_GET['region'])) {
61 | $parent_ids = $wpdb->get_col(
62 | $wpdb->prepare(
63 | "SELECT p.ID FROM $wpdb->posts p
64 | JOIN $wpdb->term_relationships r ON r.object_id = p.ID
65 | JOIN $wpdb->term_taxonomy x ON x.term_taxonomy_id = r.term_taxonomy_id
66 | WHERE x.term_id = %d",
67 | intval(sanitize_text_field($_GET['region']))
68 | )
69 | );
70 | $query->query_vars['post_parent__in'] = empty($parent_ids) ? [0] : $parent_ids;
71 | }
72 |
73 | if (!empty($_GET['type'])) {
74 | $meta_query[] = [
75 | 'key' => 'types',
76 | 'value' => '"' . sanitize_text_field($_GET['type']) . '"',
77 | 'compare' => 'LIKE',
78 | ];
79 | }
80 |
81 | if (isset($_GET['data_source']) && is_numeric($_GET['data_source'])) {
82 | $index = intval($_GET['data_source']);
83 | if (-1 === $index) {
84 | $meta_query[] = [
85 | 'key' => 'data_source',
86 | 'compare' => 'NOT EXISTS',
87 | ];
88 | } else {
89 | $data_source = array_keys($tsml_data_sources)[$index];
90 | if ($data_source) {
91 | $meta_query[] = [
92 | 'key' => 'data_source',
93 | 'value' => $data_source,
94 | 'compare' => '=',
95 | ];
96 | }
97 | }
98 | }
99 |
100 | if (!empty($meta_query)) {
101 | $query->set('meta_query', $meta_query);
102 | }
103 | }
104 | }
105 | );
106 |
107 | // custom columns for meetings
108 | add_filter(
109 | 'manage_edit-tsml_meeting_columns',
110 | function () {
111 | return [
112 | 'cb' => ' ',
113 | 'title' => __('Meeting', '12-step-meeting-list'),
114 | 'day' => __('Day', '12-step-meeting-list'),
115 | 'time' => __('Time', '12-step-meeting-list'),
116 | 'region' => __('Region', '12-step-meeting-list'),
117 | 'data_source' => __('Data Source', '12-step-meeting-list'),
118 | 'date' => __('Date', '12-step-meeting-list'),
119 | ];
120 | }
121 | );
122 |
123 | // if you're deleting meetings, also delete locations
124 | add_action(
125 | 'delete_post',
126 | function ($post_id) {
127 | $post = get_post($post_id);
128 | if ($post->post_type == 'tsml_meeting') {
129 | tsml_require_meetings_permission();
130 | tsml_delete_orphans();
131 | }
132 | }
133 | );
134 |
135 | // custom list values for meetings
136 | add_action('manage_tsml_meeting_posts_custom_column', function ($column_name, $post_ID) {
137 | global $tsml_days, $wpdb, $tsml_data_sources;
138 | if ($column_name == 'day') {
139 | $day = get_post_meta($post_ID, 'day', true);
140 | echo (empty($day) && $day !== '0') ? esc_html__('Appointment', '12-step-meeting-list') : esc_html($tsml_days[$day]);
141 | } elseif ($column_name == 'time') {
142 | echo esc_html(tsml_format_time(get_post_meta($post_ID, 'time', true)));
143 | } elseif ($column_name == 'region') {
144 | // don't know how to do this with fewer queries
145 | echo esc_html($wpdb->get_var('SELECT t.name
146 | FROM ' . $wpdb->terms . ' t
147 | JOIN ' . $wpdb->term_taxonomy . ' x ON t.term_id = x.term_id
148 | JOIN ' . $wpdb->term_relationships . ' r ON x.term_taxonomy_id = r.term_taxonomy_id
149 | JOIN ' . $wpdb->posts . ' p ON r.object_id = p.post_parent
150 | WHERE p.ID = ' . intval($post_ID)));
151 | } elseif ($column_name == 'data_source') {
152 | $data_source = get_post_meta($post_ID, 'data_source', true);
153 | if ($data_source && isset($tsml_data_sources[$data_source])) {
154 | echo $tsml_data_sources[$data_source]['name'];
155 | }
156 | }
157 | }, 10, 2);
158 |
159 |
160 | // set custom meetings columns to be sortable
161 | add_filter('manage_edit-tsml_meeting_sortable_columns', function ($columns) {
162 | $columns['day'] = 'day';
163 | $columns['time'] = 'time';
164 | return $columns;
165 | });
166 |
167 | // apply sorting
168 | add_filter('request', function ($vars) {
169 | if (isset($vars['orderby'])) {
170 | switch ($vars['orderby']) {
171 | case 'day':
172 | return array_merge($vars, [
173 | 'meta_key' => 'day',
174 | 'orderby' => 'meta_value',
175 | ]);
176 | case 'time':
177 | return array_merge($vars, [
178 | 'meta_key' => 'time',
179 | 'orderby' => 'meta_value',
180 | ]);
181 | }
182 | }
183 | return $vars;
184 | });
185 |
186 |
187 | // remove quick edit because meetings could get messed up without custom fields
188 | add_filter('post_row_actions', function ($actions) {
189 | global $post;
190 | if ($post->post_type == 'tsml_meeting') {
191 | unset($actions['inline hide-if-no-js']);
192 | }
193 | return $actions;
194 | }, 10, 2);
195 |
196 |
197 | // adding "Remove Temporary Closure" to Bulk Actions dropdown
198 | add_filter('bulk_actions-edit-tsml_meeting', function ($bulk_array) {
199 | $bulk_array['tsml_open_in_person'] = __('Reopen for In Person Attendees', '12-step-meeting-list');
200 | return $bulk_array;
201 | });
202 |
203 |
204 |
205 | // handle removing Temporary Closures
206 | add_filter('handle_bulk_actions-edit-tsml_meeting', function ($redirect, $doaction, $object_ids) {
207 | tsml_require_meetings_permission();
208 |
209 | // handle tsml_add_tc
210 | // let's remove query args first
211 | $redirect = remove_query_arg(['tsml_add_tc'], $redirect);
212 | $redirect = remove_query_arg(['tsml_remove_tc'], $redirect);
213 | $redirect = remove_query_arg(['tsml_open_in_person'], $redirect);
214 |
215 | // do something for "Add Temporary Closure" bulk action
216 | if ($doaction == 'tsml_add_tc') {
217 | $count = 0;
218 | foreach ($object_ids as $post_id) {
219 | // for each select post, add TC if it's not selected in "types"
220 | $types = get_post_meta($post_id, 'types', false)[0];
221 | if (!in_array('TC', array_values($types))) {
222 | $types[] = 'TC';
223 | update_post_meta($post_id, 'types', array_map('esc_attr', $types));
224 |
225 | $count++;
226 | }
227 | }
228 |
229 | if ($count > 0) {
230 | // rebuild cache
231 | tsml_cache_rebuild();
232 | // update types in use
233 | tsml_update_types_in_use();
234 | // add number of meetings changed to query args
235 | $redirect = add_query_arg('tsml_add_tc', $count, $redirect);
236 | }
237 | }
238 |
239 | // do something for "Remove Temporary Closure" bulk action
240 | if ($doaction == 'tsml_remove_tc') {
241 | $count = 0;
242 | foreach ($object_ids as $post_id) {
243 | // For each select post, remove TC if it's selected in "types"
244 | $types = get_post_meta($post_id, 'types', false)[0];
245 | if (!empty($types) && in_array('TC', array_values($types))) {
246 | $types = array_diff($types, ['TC']);
247 | if (empty($types)) {
248 | delete_post_meta($post_id, 'types');
249 | } else {
250 | update_post_meta($post_id, 'types', array_map('esc_attr', $types));
251 | }
252 |
253 | $count++;
254 | }
255 | }
256 |
257 | if ($count > 0) {
258 | // rebuild cache
259 | tsml_cache_rebuild();
260 | // update types in use
261 | tsml_update_types_in_use();
262 | // add number of meetings changed to query args
263 | $redirect = add_query_arg('tsml_remove_tc', $count, $redirect);
264 | }
265 | }
266 |
267 | // do something for "Remove Temporary Closure" bulk action
268 | if ($doaction == 'tsml_open_in_person') {
269 | $count = 0;
270 | foreach ($object_ids as $post_id) {
271 | $meeting = tsml_get_meeting($post_id);
272 |
273 | if ($meeting->attendance_option == 'in_person' || $meeting->attendance_option == 'hybrid') {
274 | continue;
275 | }
276 |
277 | if (empty($meeting->formatted_address) || tsml_geocode($meeting->formatted_address)['approximate'] != 'no') {
278 | continue;
279 | }
280 |
281 | // for each select post, remove TC if it's selected in "types"
282 | $types = get_post_meta($post_id, 'types', false)[0];
283 | if (!empty($types) && in_array('TC', array_values($types))) {
284 | $types = array_diff($types, ['TC']);
285 | if (empty($types)) {
286 | delete_post_meta($post_id, 'types');
287 | } else {
288 | update_post_meta($post_id, 'types', array_map('esc_attr', $types));
289 | }
290 | }
291 | $count++;
292 | }
293 |
294 | if ($count > 0) {
295 | // rebuild cache
296 | tsml_cache_rebuild();
297 | // update types in use
298 | tsml_update_types_in_use();
299 | // add number of meetings changed to query args
300 | $redirect = add_query_arg('tsml_open_in_person', $count, $redirect);
301 | }
302 | }
303 |
304 | return $redirect;
305 | }, 10, 3);
306 |
307 | // notify how many Temporary Closures were removed
308 | add_action(
309 | 'admin_notices',
310 | function () {
311 | if (!empty($_REQUEST['tsml_add_tc'])) {
312 | // depending on how many posts were changed, make the message different
313 | tsml_alert(sprintf(
314 | // translators: %s is the number of meetings that were changed
315 | _n(
316 | 'Temporary Closure added to %s meeting',
317 | 'Temporary Closure added to %s meetings',
318 | intval($_REQUEST['tsml_add_tc']),
319 | '12-step-meeting-list'
320 | ),
321 | intval($_REQUEST['tsml_add_tc'])
322 | ));
323 | }
324 | if (!empty($_REQUEST['tsml_remove_tc'])) {
325 | // depending on how many posts were changed, make the message different
326 | tsml_alert(sprintf(
327 | // translators: %s is the number of meetings that were changed
328 | _n(
329 | 'Temporary Closure removed from %s meeting',
330 | 'Temporary Closure removed from %s meetings',
331 | intval($_REQUEST['tsml_remove_tc']),
332 | '12-step-meeting-list'
333 | ),
334 | intval($_REQUEST['tsml_remove_tc'])
335 | ));
336 | }
337 | if (!empty($_REQUEST['tsml_open_in_person'])) {
338 | // depending on how many posts were changed, make the message different
339 | tsml_alert(sprintf(
340 | // translators: %s is the number of meetings that were changed
341 | _n(
342 | '%s meeting reopened for in person attendees',
343 | '%s meetings reopended for in person attendees',
344 | intval($_REQUEST['tsml_open_in_person']),
345 | '12-step-meeting-list'
346 | ),
347 | intval($_REQUEST['tsml_open_in_person'])
348 | ));
349 | }
350 | }
351 | );
352 |
353 | // special global var to store region counts
354 | $tsml_region_counts = [];
355 |
356 | // customizing the "count" column for regions - would be nice if we could use the existing 'posts' column for sorting
357 | add_filter('manage_edit-tsml_region_columns', function ($columns) {
358 | global $wpdb, $tsml_region_counts;
359 |
360 | // get region meeting counts (regions are associated with locations, not meetings)
361 | $results = $wpdb->get_results('SELECT
362 | x.term_id,
363 | (SELECT COUNT(*) FROM ' . $wpdb->posts . ' p WHERE p.post_parent IN
364 | (SELECT tr.object_id FROM ' . $wpdb->term_relationships . ' tr WHERE tr.term_taxonomy_id = x.term_taxonomy_id )
365 | ) AS meetings
366 | FROM wp_term_taxonomy x
367 | WHERE x.taxonomy = "tsml_region"');
368 | foreach ($results as $result) {
369 | $tsml_region_counts[$result->term_id] = $result->meetings;
370 | }
371 |
372 | unset($columns['posts']);
373 | $columns['meetings'] = __('Meetings', '12-step-meeting-list');
374 | return $columns;
375 | });
376 |
377 | // customizing the "count" column for regions
378 | add_action('manage_tsml_region_custom_column', function ($string, $column_name, $term_id) {
379 | global $tsml_region_counts;
380 | if ($column_name === 'meetings') {
381 | $term = get_term($term_id, 'tsml_region');
382 | $query = http_build_query([
383 | 'post_status' => 'all',
384 | 'post_type' => 'tsml_meeting',
385 | 'region' => $term->term_id,
386 | 'filter_action' => 'Filter',
387 | 'paged' => 1,
388 | ]);
389 | return '' .
390 | @$tsml_region_counts[$term->term_id] .
391 | ' ';
392 | }
393 | return $string;
394 | }, 10, 3);
395 |
--------------------------------------------------------------------------------
/includes/admin_log.php:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | $value) {
50 | if (!array_key_exists($key, $log_types)) {
51 | continue;
52 | }
53 | ?>
54 | >
55 | ()
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 1 ' : '';
17 |
18 | // add menu items
19 | add_submenu_page(
20 | 'edit.php?post_type=tsml_meeting',
21 | __('Regions', '12-step-meeting-list'),
22 | __('Regions', '12-step-meeting-list'),
23 | TSML_MEETINGS_PERMISSION,
24 | 'edit-tags.php?taxonomy=tsml_region&post_type=tsml_location'
25 | );
26 | add_submenu_page(
27 | 'edit.php?post_type=tsml_meeting',
28 | __('Districts', '12-step-meeting-list'),
29 | __('Districts', '12-step-meeting-list'),
30 | TSML_MEETINGS_PERMISSION,
31 | 'edit-tags.php?taxonomy=tsml_district&post_type=tsml_group'
32 | );
33 | add_submenu_page(
34 | 'edit.php?post_type=tsml_meeting',
35 | __('Import & Export', '12-step-meeting-list'),
36 | __('Import & Export', '12-step-meeting-list'),
37 | TSML_MEETINGS_PERMISSION,
38 | 'import',
39 | 'tsml_import_page'
40 | );
41 | add_submenu_page(
42 | 'edit.php?post_type=tsml_meeting',
43 | __('Event Log', '12-step-meeting-list'),
44 | __('Event Log', '12-step-meeting-list'),
45 | TSML_SETTINGS_PERMISSION,
46 | 'log',
47 | 'tsml_log_page'
48 | );
49 | add_submenu_page(
50 | 'edit.php?post_type=tsml_meeting',
51 | __('Settings', '12-step-meeting-list'),
52 | __('Settings', '12-step-meeting-list') . $badge,
53 | TSML_SETTINGS_PERMISSION,
54 | 'settings',
55 | 'tsml_settings_page'
56 | );
57 |
58 | // don't collapse the menu when regions or distrits are selected
59 | add_filter('parent_file', function ($parent_file) {
60 | global $submenu_file, $current_screen, $pagenow;
61 | if ($current_screen->post_type == 'tsml_location') {
62 | if ($pagenow == 'edit-tags.php') {
63 | $submenu_file = 'edit-tags.php?taxonomy=tsml_region&post_type=tsml_location';
64 | }
65 | $parent_file = 'edit.php?post_type=tsml_meeting';
66 | } elseif ($current_screen->post_type == 'tsml_group') {
67 | if ($pagenow == 'edit-tags.php') {
68 | $submenu_file = 'edit-tags.php?taxonomy=tsml_district&post_type=tsml_group';
69 | }
70 | $parent_file = 'edit.php?post_type=tsml_meeting';
71 | }
72 | return $parent_file;
73 | });
74 | });
75 |
76 | // add a widget to the main dashboard page
77 | add_action(
78 | 'wp_dashboard_setup',
79 | function () {
80 | wp_add_dashboard_widget('tsml_help_widget', '12 Step Meeting List Plugin', 'tsml_about_message', null, null, 'normal', 'high');
81 | }
82 | );
83 |
84 |
--------------------------------------------------------------------------------
/includes/admin_region.php:
--------------------------------------------------------------------------------
1 | 'tsml_region',
8 | 'hide_empty' => false,
9 | 'exclude' => $term->term_id,
10 | ]))
11 | ) {
12 | // stop if this is the only region
13 | return;
14 | }
15 | ?>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 'tsml_region',
25 | 'hierarchical' => true,
26 | 'orderby' => 'name',
27 | 'exclude' => $term->term_id,
28 | 'show_option_all' => ' ',
29 | 'name' => 'delete_and_reassign',
30 | 'id' => 'delete_and_reassign',
31 | 'hide_empty' => false
32 | ]);
33 | ?>
34 |
35 |
36 |
37 |
38 |
39 | $region_id]);
48 | foreach ($meetings as $meeting) {
49 | wp_update_post(['ID' => $meeting['id']]);
50 | }
51 |
52 | // delete this region and reassign its locations to another region
53 | if (!empty($_POST['delete_and_reassign'])) {
54 | $location_ids = get_posts([
55 | 'post_type' => 'tsml_location',
56 | 'numberposts' => -1,
57 | 'fields' => 'ids',
58 | 'tax_query' => [
59 | [
60 | 'taxonomy' => 'tsml_region',
61 | 'terms' => intval($region_id),
62 | ],
63 | ],
64 | ]);
65 |
66 | // assign new region to each location
67 | foreach ($location_ids as $location_id) {
68 | wp_set_object_terms($location_id, intval($_POST['delete_and_reassign']), 'tsml_region');
69 | }
70 |
71 | // delete term
72 | wp_delete_term($region_id, 'tsml_region');
73 |
74 | // redirect to regions list
75 | wp_safe_redirect(admin_url('edit-tags.php?taxonomy=tsml_region&post_type=tsml_location'));
76 | }
77 | },
78 | 10,
79 | 2
80 | );
81 |
--------------------------------------------------------------------------------
/includes/blocks.php:
--------------------------------------------------------------------------------
1 | run();
9 |
--------------------------------------------------------------------------------
/includes/blocks/class-tsml-blocks.php:
--------------------------------------------------------------------------------
1 | data_source = strval($arguments['data_source']);
30 | }
31 |
32 | if (!empty($arguments['day']) || (isset($arguments['day']) && $arguments['day'] == 0)) {
33 | $this->day = is_array($arguments['day']) ? array_map('intval', $arguments['day']) : [intval($arguments['day'])];
34 | }
35 |
36 | if (!empty($arguments['district'])) {
37 | $this->district_id = is_array($arguments['district']) ? array_map('sanitize_title', $arguments['district']) : [sanitize_title($arguments['district'])];
38 | // we are recieving district slugs, need to convert to IDs (todo save this in the cache)
39 | $this->district_id = array_map([$this, 'get_district_id'], $this->district_id);
40 | // district_id is now an array of arrays because districts can have children
41 | $return = [];
42 | foreach ($this->district_id as $district_id_array) {
43 | $return = array_merge($return, $district_id_array);
44 | }
45 | $this->district_id = $return;
46 | }
47 |
48 | if (!empty($arguments['group_id'])) {
49 | $this->group_id = is_array($arguments['group_id']) ? array_map('intval', $arguments['group_id']) : [intval($arguments['group_id'])];
50 | }
51 |
52 | if (!empty($arguments['latitude']) && !empty($arguments['longitude'])) {
53 | $this->latitude = floatval($arguments['latitude']);
54 | $this->longitude = floatval($arguments['longitude']);
55 | $this->distance_units = (!empty($arguments['longitude']) && $arguments['longitude'] == 'km') ? 'km' : 'mi';
56 | if (!empty($arguments['distance'])) {
57 | $this->distance = floatval($arguments['distance']);
58 | }
59 | }
60 |
61 | if (!empty($arguments['location_id'])) {
62 | $this->location_id = is_array($arguments['location_id']) ? array_map('intval', $arguments['location_id']) : [intval($arguments['location_id'])];
63 | }
64 |
65 | if (!empty($arguments['query'])) {
66 | $this->searchable_keys = ['name', 'notes', 'location', 'location_notes', 'formatted_address', 'group', 'group_notes', 'region', 'sub_region', 'district', 'sub_district'];
67 | $this->query = array_map('sanitize_text_field', array_filter(array_unique(explode(' ', stripslashes($arguments['query'])))));
68 | }
69 |
70 | if (!empty($arguments['region'])) {
71 | $this->region_id = is_array($arguments['region']) ? array_map('sanitize_title', $arguments['region']) : [sanitize_title($arguments['region'])];
72 | // we are recieving region slugs, need to convert to IDs (todo save this in the cache)
73 | $this->region_id = array_map([$this, 'get_region_id'], $this->region_id);
74 | // region_id is now an array of arrays because regions can have children
75 | $return = [];
76 | foreach ($this->region_id as $region_id_array) {
77 | $return = array_merge($return, $region_id_array);
78 | }
79 | $this->region_id = $return;
80 | }
81 |
82 | if (!empty($arguments['time'])) {
83 | $this->time = is_array($arguments['time']) ? array_map('sanitize_title', $arguments['time']) : [sanitize_title($arguments['time'])];
84 | if (in_array('upcoming', $this->time)) {
85 | $this->ten_minutes_ago = date('H:i', current_time('U') - 600);
86 | }
87 | }
88 |
89 | if (!empty($arguments['type'])) {
90 | $this->type = is_array($arguments['type']) ? array_map('trim', $arguments['type']) : explode(',', trim($arguments['type']));
91 | }
92 |
93 | if (!empty($arguments['attendance_option'])) {
94 | $this->attendance_option = is_array($arguments['attendance_option'])
95 | ? array_map('trim', $arguments['attendance_option'])
96 | : explode(',', trim($arguments['attendance_option']));
97 | if (!empty(array_intersect($this->attendance_option, ['online', 'in_person']))) {
98 | $this->attendance_option[] = 'hybrid';
99 | }
100 | if (in_array('active', $this->attendance_option)) {
101 | $this->attendance_option = ['hybrid', 'in_person', 'online'];
102 | }
103 | $this->attendance_option = array_unique($this->attendance_option);
104 | }
105 | }
106 |
107 | // run the filters
108 | public function apply($meetings)
109 | {
110 |
111 | // run filters
112 | if ($this->data_source) {
113 | $meetings = array_filter($meetings, [$this, 'filter_data_source']);
114 | }
115 |
116 | if ($this->day) {
117 | $meetings = array_filter($meetings, [$this, 'filter_day']);
118 | }
119 |
120 | if ($this->district_id) {
121 | $meetings = array_filter($meetings, [$this, 'filter_district']);
122 | }
123 |
124 | if ($this->group_id) {
125 | $meetings = array_filter($meetings, [$this, 'filter_group']);
126 | }
127 |
128 | if ($this->location_id) {
129 | $meetings = array_filter($meetings, [$this, 'filter_location']);
130 | }
131 |
132 | if ($this->query) {
133 | $meetings = array_filter($meetings, [$this, 'filter_query']);
134 | }
135 |
136 | if ($this->region_id) {
137 | $meetings = array_filter($meetings, [$this, 'filter_region']);
138 | }
139 |
140 | if ($this->time) {
141 | $meetings = array_filter($meetings, [$this, 'filter_time']);
142 | }
143 |
144 | if ($this->type) {
145 | $meetings = array_filter($meetings, [$this, 'filter_type']);
146 | }
147 |
148 | // if lat and lon are set then compute distances
149 | if ($this->latitude && $this->longitude) {
150 | $meetings = array_map([$this, 'calculate_distance'], $meetings);
151 | if ($this->distance) {
152 | $meetings = array_filter($meetings, [$this, 'filter_distance']);
153 | }
154 | }
155 |
156 | if ($this->attendance_option) {
157 | $meetings = array_filter($meetings, [$this, 'filter_attendance_option']);
158 | }
159 |
160 | // return data
161 | return array_values($meetings);
162 | }
163 |
164 | // calculate distance to meeting
165 | public function calculate_distance($meeting)
166 | {
167 | if (!isset($meeting['latitude']) || !isset($meeting['longitude'])) {
168 | return $meeting;
169 | }
170 |
171 | $meeting['distance'] = rad2deg(acos(sin(deg2rad($this->latitude)) * sin(deg2rad($meeting['latitude'])) + cos(deg2rad($this->latitude)) * cos(deg2rad($meeting['latitude'])) * cos(deg2rad($this->longitude - $meeting['longitude'])))) * 69.09;
172 | if ($this->distance_units == 'km') {
173 | $meeting['distance'] *= 1.609344;
174 | }
175 |
176 | $meeting['distance'] = round($meeting['distance'], 1);
177 | return $meeting;
178 | }
179 |
180 | // callback function to pass to array_filter
181 | public function filter_data_source($meeting)
182 | {
183 | return isset($meeting['data_source']) ? ($this->data_source === $meeting['data_source']) : false;
184 | }
185 |
186 | // callback function to pass to array_filter
187 | public function filter_day($meeting)
188 | {
189 | if (!isset($meeting['day'])) {
190 | return false;
191 | }
192 |
193 | return in_array($meeting['day'], $this->day);
194 | }
195 |
196 | // callback function to pass to array_filter
197 | public function filter_distance($meeting)
198 | {
199 | if (!isset($meeting['distance'])) {
200 | return false;
201 | }
202 |
203 | return $meeting['distance'] < $this->distance;
204 | }
205 |
206 | // callback function to pass to array_filter
207 | public function filter_district($meeting)
208 | {
209 | if (!isset($meeting['district_id'])) {
210 | return false;
211 | }
212 |
213 | return in_array($meeting['district_id'], $this->district_id);
214 | }
215 |
216 | // callback function to pass to array_filter
217 | public function filter_group($meeting)
218 | {
219 | if (!isset($meeting['group_id'])) {
220 | return false;
221 | }
222 |
223 | return in_array($meeting['group_id'], $this->group_id);
224 | }
225 |
226 | // callback function to pass to array_filter
227 | public function filter_location($meeting)
228 | {
229 | if (!isset($meeting['location_id'])) {
230 | return false;
231 | }
232 |
233 | return in_array($meeting['location_id'], $this->location_id);
234 | }
235 |
236 | // callback function to pass to array_filter
237 | public function filter_query($meeting)
238 | {
239 | foreach ($this->query as $word) {
240 | $word_matches = false;
241 | foreach ($this->searchable_keys as $key) {
242 | if (isset($meeting[$key]) && stripos($meeting[$key], $word) !== false) {
243 | $word_matches = true;
244 | break;
245 | }
246 | }
247 | if (!$word_matches) {
248 | return false;
249 | }
250 | }
251 | return true;
252 | }
253 |
254 | // callback function to pass to array_filter
255 | public function filter_region($meeting)
256 | {
257 | if (!isset($meeting['region_id'])) {
258 | return false;
259 | }
260 |
261 | return in_array($meeting['region_id'], $this->region_id);
262 | }
263 |
264 | // callback function to pass to array_filter
265 | public function filter_time($meeting)
266 | {
267 | if (!isset($meeting['time'])) {
268 | return false;
269 | }
270 |
271 | foreach ($this->time as $time) {
272 | if ($time == 'morning') {
273 | return (strcmp('04:00', $meeting['time']) <= 0 && strcmp('11:59', $meeting['time']) >= 0);
274 | } elseif ($time == 'midday') {
275 | return (strcmp('11:00', $meeting['time']) <= 0 && strcmp('16:59', $meeting['time']) >= 0);
276 | } elseif ($time == 'evening') {
277 | return (strcmp('16:00', $meeting['time']) <= 0 && strcmp('20:59', $meeting['time']) >= 0);
278 | } elseif ($time == 'night') {
279 | return (strcmp('20:00', $meeting['time']) <= 0 || strcmp('04:59', $meeting['time']) >= 0);
280 | } elseif ($time == 'upcoming') {
281 | return (strcmp($this->ten_minutes_ago, $meeting['time']) <= 0);
282 | }
283 | }
284 | }
285 |
286 | // callback function to pass to array_filter
287 | public function filter_type($meeting)
288 | {
289 | if (!isset($meeting['types'])) {
290 | return false;
291 | }
292 | return !count(array_diff($this->type, $meeting['types']));
293 | }
294 |
295 | // callback function to pass to array_filter
296 | public function filter_attendance_option($meeting)
297 | {
298 | if (!isset($meeting['attendance_option'])) {
299 | return false;
300 | }
301 | return in_array($meeting['attendance_option'], $this->attendance_option);
302 | }
303 |
304 | // function to get district id from slug
305 | public function get_district_id($slug)
306 | {
307 | $term = get_term_by('slug', $slug, 'tsml_district');
308 | $children = get_term_children($term->term_id, 'tsml_district');
309 | return array_merge([$term->term_id], $children);
310 | }
311 |
312 | // function to get region id from slug, as well as child region ids
313 | public function get_region_id($slug)
314 | {
315 | $term = get_term_by('slug', $slug, 'tsml_region');
316 | if (empty($term->term_id)) {
317 | return [];
318 | }
319 | $children = get_term_children($term->term_id, 'tsml_region');
320 | return array_merge([$term->term_id], $children);
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/includes/functions_format.php:
--------------------------------------------------------------------------------
1 | 1) {
18 | $state_zip = array_pop($parts);
19 | $parts[count($parts) - 1] .= ', ' . $state_zip;
20 | }
21 | }
22 | if ($street_only) {
23 | return array_shift($parts);
24 | }
25 | return implode(' ', $parts);
26 | }
27 |
28 | /**
29 | * takes 0, 18:30 and returns Sunday, 6:30 pm (depending on your settings)
30 | * used on admin_edit.php, archive-meetings.php, single-meetings.php
31 | *
32 | * @param mixed $day
33 | * @param mixed $time
34 | * @param mixed $separator
35 | * @param mixed $short
36 | * @return string
37 | */
38 | function tsml_format_day_and_time($day, $time, $separator = ', ', $short = false)
39 | {
40 | global $tsml_days;
41 | // translators: Appt is abbreviation for Appointment
42 | if (empty($tsml_days[$day]) || empty($time)) {
43 | return $short ? __('Appt', '12-step-meeting-list') : __('Appointment', '12-step-meeting-list');
44 | }
45 | return ($short ? substr($tsml_days[$day], 0, 3) : $tsml_days[$day]) . $separator . tsml_format_time($time);
46 | }
47 |
48 | /**
49 | * appends men or women (or custom flags) if type present
50 | * used on archive-meetings.php
51 | *
52 | * @param mixed $name
53 | * @param mixed $types
54 | * @return mixed
55 | */
56 | function tsml_format_name($name, $types = null)
57 | {
58 | global $tsml_program, $tsml_programs;
59 | if (!is_array($types)) {
60 | $types = [];
61 | }
62 | if (empty($tsml_programs[$tsml_program]['flags']) || !is_array($tsml_programs[$tsml_program]['flags'])) {
63 | return $name;
64 | }
65 | $append = [];
66 | $meeting_is_online = in_array('ONL', $types);
67 | // Types assigned to the meeting passed to the function
68 | foreach ($types as $type) {
69 | // True if the type for the meeting exists in one of the predetermined flags
70 | $type_is_flagged = in_array($type, $tsml_programs[$tsml_program]['flags']);
71 | $type_not_tc_and_online = !($type === 'TC' && $meeting_is_online);
72 |
73 | if ($type_is_flagged && $type_not_tc_and_online) {
74 | $append[] = $tsml_programs[$tsml_program]['types'][$type];
75 | }
76 | }
77 | return count($append) ? $name . ' ' . implode(', ', $append) . ' ' : $name;
78 | }
79 |
80 | /**
81 | * format notes with sanitized paragraphs and line breaks
82 | *
83 | * @param mixed $notes
84 | * @return void
85 | */
86 | function tsml_format_notes($notes)
87 | {
88 | echo wpautop(nl2br(esc_html($notes)));
89 | }
90 |
91 | /**
92 | * get meeting types
93 | * used on archive-meetings.php
94 | *
95 | * @param mixed $types
96 | * @return string
97 | */
98 | function tsml_format_types($types = [])
99 | {
100 | global $tsml_program, $tsml_programs;
101 | if (!is_array($types)) {
102 | $types = [];
103 | }
104 | $append = [];
105 | // Types assigned to the meeting passed to the function
106 | foreach ($types as $type) {
107 | // True if the type for the meeting exists in one of the predetermined flags
108 | $type_is_flagged = in_array($type, $tsml_programs[$tsml_program]['flags']);
109 |
110 | if ($type_is_flagged && $type != 'TC' && $type != 'ONL') {
111 | $append[] = $tsml_programs[$tsml_program]['types'][$type];
112 | }
113 | }
114 |
115 | return implode(', ', $append);
116 | }
117 |
118 | /**
119 | * takes 18:30 and returns 6:30 pm (depending on your settings)
120 | * used on tsml_get_meetings(), single-meetings.php, admin_lists.php
121 | *
122 | * @param mixed $string
123 | * @return string
124 | */
125 | function tsml_format_time($string)
126 | {
127 | if (empty($string)) {
128 | return __('Appointment', '12-step-meeting-list');
129 | }
130 | if ($string == '12:00') {
131 | return __('Noon', '12-step-meeting-list');
132 | }
133 | if ($string == '23:59' || $string == '00:00') {
134 | return __('Midnight', '12-step-meeting-list');
135 | }
136 | $date = strtotime($string);
137 | return date(get_option('time_format'), $date);
138 | }
139 |
140 | /**
141 | * takes a time string, eg 6:30 pm, and returns 18:30
142 | * used on tsml_import(), tsml_time_duration()
143 | *
144 | * @param mixed $string
145 | * @return string
146 | */
147 | function tsml_format_time_reverse($string)
148 | {
149 | $time_parts = date_parse($string);
150 | return sprintf('%02d', $time_parts['hour']) . ':' . sprintf('%02d', $time_parts['minute']);
151 | }
152 |
153 | /**
154 | * takes a website URL, eg https://www.groupname.org and returns the domain
155 | * used on single-meetings.php
156 | *
157 | * @param mixed $url
158 | * @return mixed
159 | */
160 | function tsml_format_domain($url)
161 | {
162 | $parts = parse_url(strtolower($url));
163 | if (!$parts) {
164 | return $url;
165 | }
166 | if (substr($parts['host'], 0, 4) == 'www.') {
167 | return substr($parts['host'], 4);
168 | }
169 | return $parts['host'];
170 | }
171 |
--------------------------------------------------------------------------------
/includes/functions_input.php:
--------------------------------------------------------------------------------
1 | $value) {
14 | if ($value) {
15 | echo ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
16 | }
17 | }
18 | echo '>';
19 | }
20 |
21 | /**
22 | * render a date input field
23 | *
24 | * @param mixed $name
25 | * @param mixed $value
26 | * @param mixed $attributes
27 | * @return void
28 | */
29 | function tsml_input_date($name, $value = '', $attributes = [])
30 | {
31 | tsml_input(array_merge(['id' => $name, 'name' => $name, 'type' => 'date', 'value' => $value], $attributes));
32 | }
33 |
34 | /**
35 | * render an email input field
36 | *
37 | * @param mixed $name
38 | * @param mixed $value
39 | * @param mixed $attributes
40 | * @return void
41 | */
42 | function tsml_input_email($name, $value = '', $attributes = [])
43 | {
44 | tsml_input(array_merge(['id' => $name, 'name' => $name, 'type' => 'email', 'value' => $value], $attributes));
45 | }
46 |
47 | /**
48 | * render a hidden input field
49 | *
50 | * @param mixed $name
51 | * @param mixed $value
52 | * @param mixed $attributes
53 | * @return void
54 | */
55 | function tsml_input_hidden($name, $value = '', $attributes = [])
56 | {
57 | tsml_input(array_merge(['id' => $name, 'name' => $name, 'type' => 'hidden', 'value' => $value], $attributes));
58 | }
59 |
60 | /**
61 | * render a submit button
62 | *
63 | * @param mixed $value
64 | * @param mixed $attributes
65 | * @return void
66 | */
67 | function tsml_input_submit($value, $attributes = ['class' => 'button'])
68 | {
69 | tsml_input(array_merge(['type' => 'submit', 'value' => $value], $attributes));
70 | }
71 |
72 | /**
73 | * render a text input field
74 | * @param mixed $name
75 | * @param mixed $value
76 | * @param mixed $attributes
77 | * @return void
78 | */
79 | function tsml_input_text($name, $value = '', $attributes = [])
80 | {
81 | tsml_input(array_merge(['id' => $name, 'name' => $name, 'type' => 'text', 'value' => $value], $attributes));
82 | }
83 |
84 | /**
85 | * render a url field
86 | *
87 | * @param mixed $name
88 | * @param mixed $value
89 | * @param mixed $attributes
90 | * @return void
91 | */
92 | function tsml_input_url($name, $value = '', $attributes = ['placeholder' => 'https://'])
93 | {
94 | tsml_input(array_merge(['id' => $name, 'name' => $name, 'type' => 'url', 'value' => $value], $attributes));
95 | }
96 |
--------------------------------------------------------------------------------
/includes/functions_log.php:
--------------------------------------------------------------------------------
1 | __('Data source', '12-step-meeting-list'),
6 | 'data_source_error' => __('Data source error', '12-step-meeting-list'),
7 | 'import_meeting' => __('Meeting import', '12-step-meeting-list'),
8 | 'geocode_success' => __('Geocoding success', '12-step-meeting-list'),
9 | 'geocode_error' => __('Geocoding error', '12-step-meeting-list'),
10 | 'geocode_connection_error' => __('Geocoding connection error', '12-step-meeting-list'),
11 | ]);
12 |
13 | /**
14 | * add an entry to the activity log
15 | * used in tsml_ajax_info, tsml_geocode and anywhere else something could go wrong
16 | *
17 | * @param mixed $type something short you can filter by, eg 'geocode_error'
18 | * @param mixed $info the bad result you got back
19 | * @param mixed $input any input that might have contributed to the result
20 | * @return void
21 | */
22 | function tsml_log($type, $info = null, $input = null)
23 | {
24 | global $tsml_log_updates;
25 |
26 | // to avoid too many db read / writes, save new entries
27 | // and write to option on shutdown
28 | if (!is_array($tsml_log_updates)) {
29 | $tsml_log_updates = [];
30 | }
31 |
32 | // default variables
33 | $entry = [
34 | 'type' => $type,
35 | 'timestamp' => time(),
36 | ];
37 |
38 | // optional variables
39 | if ($info) {
40 | $entry['info'] = $info;
41 | }
42 | if (!empty($input)) {
43 | if (is_array($input)) {
44 | $entry = array_merge($entry, $input);
45 | } else {
46 | $entry['input'] = $input;
47 | }
48 | }
49 |
50 | // prepend to array
51 | array_unshift($tsml_log_updates, $entry);
52 |
53 | // Check if the WordPress action hook 'shutdown' has been set
54 | if (!has_action('shutdown', 'tsml_log_save_updates')) {
55 | add_action('shutdown', 'tsml_log_save_updates');
56 | }
57 | }
58 |
59 | /**
60 | * Summary of tsml_log_save
61 | * @return void
62 | */
63 | function tsml_log_save_updates()
64 | {
65 | global $tsml_log_updates;
66 | if (is_array($tsml_log_updates) && !empty($tsml_log_updates)) {
67 | $tsml_log = tsml_get_option_array('tsml_log');
68 | $tsml_log = array_filter($tsml_log, 'is_array');
69 | // trim to last 30 days
70 | $cutoff = time() - (30 * 24 * 60 * 60);
71 | $tsml_log = array_filter($tsml_log, function ($entry) use ($cutoff) {
72 | if (!is_array($entry) || !isset($entry['timestamp'])) {
73 | return false;
74 | }
75 | // previous timestamps are mysqldate strings, new are epoch numbers
76 | $time = intval($entry['timestamp']);
77 | return $time && $cutoff < $time;
78 | });
79 | // add updates
80 | $tsml_log = array_merge($tsml_log_updates, $tsml_log);
81 |
82 | // upper limit to log entries
83 | $tsml_log = array_slice($tsml_log, 0, 1000);
84 |
85 | update_option('tsml_log', $tsml_log);
86 | }
87 | }
88 |
89 | /**
90 | * get log entries
91 | * @param array $args [optional] args to filters results
92 | * [type] event type to filter
93 | * [count] limit how many returned
94 | * [start] offset to start returning
95 | */
96 | function tsml_log_get($args = array())
97 | {
98 | $args = (array) $args;
99 | $tsml_log = tsml_get_option_array('tsml_log');
100 | $tsml_log = array_filter($tsml_log, 'is_array');
101 |
102 | if (isset($args['type'])) {
103 | $entry_type = strval($args['type']);
104 | $tsml_log = array_filter($tsml_log, function ($entry) use ($entry_type) {
105 | return isset($entry['type']) && $entry['type'] === $entry_type;
106 | });
107 | }
108 | $count = isset($args['count']) ? intval($args['count']) : 0;
109 | $start = isset($args['start']) ? intval($args['start']) : 0;
110 |
111 | if ($count) {
112 | $tsml_log = array_slice($tsml_log, $start, $count);
113 | }
114 | return $tsml_log;
115 | }
116 |
117 | /**
118 | * Return string output for log entry message (input)
119 | * @param array $entry
120 | * @return string
121 | */
122 | function tsml_log_format_entry_msg($entry)
123 | {
124 | $entry = (array) $entry;
125 | $msg = !empty($entry['input']) ? $entry['input'] : '';
126 | if ($msg && !empty($entry['meeting_id'])) {
127 | $url = admin_url('post.php?post=' . intval($entry['meeting_id']) . '&action=edit');
128 | $msg = '' . $msg . ' ';
129 | }
130 | return $msg;
131 | }
--------------------------------------------------------------------------------
/includes/functions_timezone.php:
--------------------------------------------------------------------------------
1 | 'UTC'];
37 | ?>
38 |
39 | >
40 | $cities) { ?>
41 |
42 | $city) { ?>
43 | >
44 |
45 |
46 |
47 |
48 |
49 |
50 | post_type === 'tsml_meeting') {
39 |
40 | // when TSML UI is enabled, redirect legacy meeting detail page to TSML UI detail page
41 | if ($tsml_user_interface === 'tsml_ui') {
42 | $mtg_permalink = get_post_type_archive_link('tsml_meeting');
43 | wp_redirect(add_query_arg('meeting', $post->post_name, $mtg_permalink));
44 | exit;
45 | }
46 |
47 | // user has a custom meeting detail page
48 | $user_theme_file = get_stylesheet_directory() . '/single-meetings.php';
49 | if (file_exists($user_theme_file)) {
50 | return $user_theme_file;
51 | }
52 |
53 | // show legacy meeting detail page
54 | return dirname(__FILE__) . '/../templates/single-meetings.php';
55 | } elseif ($post->post_type == 'tsml_location') {
56 |
57 | // when TSML UI is enabled, redirect legacy location page to main meetings page
58 | if ($tsml_user_interface == 'tsml_ui') {
59 | $mtg_permalink = get_post_type_archive_link('tsml_meeting');
60 | wp_redirect($mtg_permalink);
61 | exit;
62 | }
63 |
64 | // user has a custom location detail page
65 | $user_theme_file = get_stylesheet_directory() . '/single-locations.php';
66 | if (file_exists($user_theme_file)) {
67 | return $user_theme_file;
68 | }
69 |
70 | // show legacy location detail page
71 | return dirname(__FILE__) . '/../templates/single-locations.php';
72 | }
73 | return $template;
74 | });
75 |
76 |
77 | // add theme name to body class, for per-theme CSS fixes
78 | add_filter('body_class', function ($classes) {
79 | $theme = wp_get_theme();
80 | $classes[] = sanitize_title($theme->Template);
81 | return $classes;
82 | });
83 |
84 | });
85 |
86 | if (is_admin()) {
87 | // Rebuild tsml cache when using bulk actions to edit status.
88 | // Uses a transient to keep track the array of post ids that are being processed
89 | add_action('transition_post_status', function ($new_status, $old_status, $post) {
90 | global $pagenow;
91 | if ($post->post_type === 'tsml_meeting' && $new_status === 'publish' && $pagenow !== 'post.php' && $pagenow !== 'admin-ajax.php') {
92 | tsml_require_meetings_permission();
93 |
94 | // Try to load transient first or $_GET['post'] if it doesn't exist
95 | if ($bulk_ids = get_transient('tsml_bulk_process')) {
96 | // Remove current post ID from array
97 | if (($key = array_search($post->ID, $bulk_ids)) !== false) {
98 | unset($bulk_ids[$key]);
99 | }
100 | // Resave transient
101 | set_transient('tsml_bulk_process', $bulk_ids, HOUR_IN_SECONDS);
102 | // If the array is empty, we are done. Process now.
103 | if (empty($bulk_ids)) {
104 | tsml_update_types_in_use();
105 | tsml_cache_rebuild();
106 | delete_transient('tsml_bulk_process');
107 | }
108 | } else {
109 | if (!empty($_GET['post'])) {
110 | $bulk_ids = $_GET['post'];
111 | // Remove current post ID from array
112 | if (($key = array_search($post->ID, $bulk_ids)) !== false) {
113 | unset($bulk_ids[$key]);
114 | }
115 | // Set transient. Expire in 5 minutes.
116 | set_transient('tsml_bulk_process', $bulk_ids, 5 * MINUTE_IN_SECONDS);
117 | }
118 | }
119 | }
120 | }, 10, 3);
121 | // rebuild cache when trashing or untrashing posts
122 | add_action('trashed_post', 'tsml_trash_change');
123 | add_action('untrashed_post', 'tsml_trash_change');
124 | function tsml_trash_change($post_id)
125 | {
126 | if (get_post_type($post_id) === 'tsml_meeting') {
127 | tsml_cache_rebuild();
128 | }
129 | }
130 | } else {
131 | // add plugin version number to header on public site
132 | add_action('wp_head', function () {
133 | global $tsml_sharing;
134 | echo ' ' . PHP_EOL;
135 | if ($tsml_sharing == 'open') {
136 | echo ' ' . PHP_EOL;
137 | }
138 | });
139 | }
140 |
--------------------------------------------------------------------------------
/includes/shortcodes.php:
--------------------------------------------------------------------------------
1 | 5, 'message' => ''], $arguments, 'tsml_next_meetings');
15 | $meetings = tsml_get_meetings([
16 | 'day' => intval(current_time('w')),
17 | 'time' => 'upcoming',
18 | 'attendance_option' => 'active',
19 | ]);
20 | if (!count($meetings) && empty($arguments['message'])) {
21 | return false;
22 | }
23 | if (!count($meetings) && !empty($arguments['message'])) {
24 | return '' . $arguments['message'] . '
';
25 | }
26 |
27 | $meetings = array_slice($meetings, 0, $arguments['count']);
28 | $rows = '';
29 | foreach ($meetings as $meeting) {
30 | $meeting_types = $classes = '';
31 | if (!empty($meeting['types'])) {
32 | $classes = tsml_to_css_classes($meeting['types']);
33 | $meeting_types = ' ' . tsml_format_types($meeting['types']) . ' ';
34 | }
35 |
36 | if (!empty($meeting['notes'])) {
37 | $classes .= ' notes';
38 | }
39 |
40 | $meeting_location = '';
41 | if (!empty($meeting['location'])) {
42 | $meeting_location = $meeting['location'];
43 | }
44 |
45 | if ($meeting['attendance_option'] == 'online' || $meeting['attendance_option'] == 'inactive') {
46 | $meeting_location = !empty($meeting['group']) ? $meeting['group'] : '';
47 | }
48 |
49 | $region = '';
50 | if (!empty($meeting['sub_region'])) {
51 | $region = $meeting['sub_region'];
52 | } elseif (!empty($meeting['region'])) {
53 | $region = $meeting['region'];
54 | }
55 |
56 | $rows .= '
57 | ' . tsml_format_time($meeting['time']) . '
58 | ' . @$meeting['name'] . ' ' . $meeting_types . '
59 |
60 | ' . $meeting_location . '
61 | ' . ($meeting['attendance_option'] != 'in_person' ? $tsml_meeting_attendance_options[$meeting['attendance_option']] : '') . '
62 |
63 | ' . $region . '
64 | ';
65 | }
66 | return '
67 |
77 |
78 |
79 |
80 | ' . __('Time', '12-step-meeting-list') . '
81 | ' . __('Meeting', '12-step-meeting-list') . '
82 | ' . __('Location', '12-step-meeting-list') . '
83 | ' . __('Region', '12-step-meeting-list') . '
84 |
85 |
86 | ' . $rows . '
87 |
';
88 | }
89 | add_shortcode('tsml_next_meetings', 'tsml_next_meetings');
90 |
91 | // output a list of types with links for AA-DC
92 | add_shortcode('tsml_types_list', function () {
93 | global $tsml_types_in_use, $tsml_programs, $tsml_program, $tsml_user_interface;
94 | $types = [];
95 | foreach ($tsml_types_in_use as $type) {
96 | if ($tsml_user_interface === 'tsml_ui') {
97 | $filter_url = tsml_meetings_url([
98 | 'type' => str_replace(' ', '-', strtolower($tsml_programs[$tsml_program]['types'][$type]))
99 | ]);
100 | } else {
101 | $filter_url = tsml_meetings_url(['tsml-day' => 'any', 'tsml-type' => $type]);
102 | }
103 | $types[$tsml_programs[$tsml_program]['types'][$type]] = '' . $tsml_programs[$tsml_program]['types'][$type] . ' ';
104 | }
105 | ksort($types);
106 | return 'Types ';
107 | });
108 |
109 | // output a react meeting finder widget https://github.com/code4recovery/tsml-ui
110 | function tsml_ui($arguments = [])
111 | {
112 | global $tsml_mapbox_key, $tsml_nonce, $tsml_conference_providers, $tsml_language, $tsml_programs, $tsml_program, $tsml_ui_config,
113 | $tsml_feedback_addresses, $tsml_cache, $tsml_cache_writable, $tsml_distance_units, $tsml_columns, $tsml_timezone;
114 |
115 | $defaults = shortcode_atts([
116 | 'distance' => '',
117 | 'mode' => '',
118 | 'region' => '',
119 | 'search' => '',
120 | 'time' => '',
121 | 'type' => '',
122 | 'view' => '',
123 | 'weekday' => '',
124 | ], $arguments, 'tsml_ui');
125 |
126 | // sanitize arrays
127 | foreach (['region', 'time', 'type', 'weekday'] as $key) {
128 | $defaults[$key] = explode(',', $defaults[$key]);
129 | $defaults[$key] = array_map('sanitize_title', $defaults[$key]);
130 | $defaults[$key] = array_filter($defaults[$key]);
131 | }
132 |
133 | // sanitize search
134 | $defaults['search'] = sanitize_text_field($defaults['search']);
135 |
136 | // view must either be table or map
137 | if (!in_array($defaults['view'], ['table', 'map'])) {
138 | $defaults['view'] = 'table';
139 | }
140 |
141 | // mode must either be search, location, or me
142 | if (!in_array($defaults['mode'], ['search', 'location', 'me'])) {
143 | $defaults['mode'] = 'search';
144 | }
145 |
146 | // distance must be an integer
147 | $defaults['distance'] = intval($defaults['distance']);
148 | $defaults['distance'] = in_array($defaults['distance'], [1, 2, 5, 10, 15, 25, 50, 100])
149 | ? [strval($defaults['distance'])]
150 | : [];
151 |
152 |
153 | // enqueue app script
154 | $js = defined('TSML_UI_PATH') ? TSML_UI_PATH : 'https://tsml-ui.code4recovery.org/app.js';
155 | wp_enqueue_script('tsml_ui', $js, [], TSML_VERSION, ['in_footer' => true, 'strategy' => 'async']);
156 |
157 | // get program types and type descriptions
158 | $types = !empty($tsml_programs[$tsml_program]['types'])
159 | ? $tsml_programs[$tsml_program]['types']
160 | : [];
161 | $type_descriptions = !empty($tsml_programs[$tsml_program]['type_descriptions'])
162 | ? $tsml_programs[$tsml_program]['type_descriptions']
163 | : [];
164 |
165 | // apply settings
166 | wp_localize_script(
167 | 'tsml_ui',
168 | 'tsml_react_config',
169 | array_merge(
170 | [
171 | 'columns' => array_keys($tsml_columns),
172 | 'defaults' => $defaults,
173 | 'conference_providers' => $tsml_conference_providers,
174 | 'distance_unit' => $tsml_distance_units,
175 | 'feedback_emails' => array_values($tsml_feedback_addresses),
176 | 'flags' => $tsml_programs[$tsml_program]['flags'],
177 | 'strings' => [
178 | $tsml_language => array_merge($tsml_columns, compact('types', 'type_descriptions')),
179 | ],
180 | ],
181 | $tsml_ui_config
182 | )
183 | );
184 |
185 | // use meetings.json if it's writable, otherwise use the admin-ajax URL to the feed
186 | $data = $tsml_cache_writable && file_exists(WP_CONTENT_DIR . $tsml_cache)
187 | ? content_url($tsml_cache) . '?' . filemtime(WP_CONTENT_DIR . $tsml_cache)
188 | : admin_url('admin-ajax.php') . '?action=meetings&nonce=' . wp_create_nonce($tsml_nonce);
189 |
190 | // remove URL domain to fix CORS issues caused by GoDaddy Managed WP
191 | $url = parse_url($data);
192 | $data = $url['path'] . '?' . $url['query'];
193 |
194 | return '
';
198 | }
199 | add_shortcode('tsml_react', 'tsml_ui');
200 | add_shortcode('tsml_ui', 'tsml_ui');
201 |
202 | // output a list of regions with links for AA-DC
203 | add_shortcode('tsml_regions_list', function () {
204 | // run function recursively
205 | function get_regions($parent = 0)
206 | {
207 | global $tsml_user_interface;
208 | $taxonomy = 'tsml_region';
209 | // phpcs:ignore
210 | $terms = get_terms(compact('taxonomy', 'parent'));
211 | if (!count($terms)) {
212 | return;
213 | }
214 |
215 | foreach ($terms as &$term) {
216 | if ($tsml_user_interface === 'tsml_ui') {
217 | $filter_url = tsml_meetings_url(['region' => $term->slug]);
218 | } else {
219 | $filter_url = tsml_meetings_url(
220 | ['tsml-day' => 'any', 'tsml-region' => $term->slug]
221 | );
222 | }
223 | $term = '' . $term->name . ' ' . get_regions($term->term_id) . ' ';
224 | }
225 | return '';
226 | }
227 |
228 | return 'Regions ' . get_regions();
229 | });
230 |
--------------------------------------------------------------------------------
/includes/widgets.php:
--------------------------------------------------------------------------------
1 | __('Display a table of upcoming meetings.', '12-step-meeting-list'),
15 | ]
16 | );
17 | }
18 |
19 | // front-end display of widget
20 | public function widget($args, $instance)
21 | {
22 | global $tsml_user_interface;
23 |
24 | $table = tsml_next_meetings($instance);
25 | if (empty($table)) {
26 | return false;
27 | }
28 |
29 | if (empty($instance['title'])) {
30 | $instance['title'] = '';
31 | }
32 |
33 | if (!empty($instance['css'])) {
34 | echo '';
103 | }
104 |
105 | // don't know how to set this properly
106 | $args['before_widget'] = str_replace(' class="', ' class="tsml-widget-upcoming ', $args['before_widget']);
107 |
108 | $output = $args['before_widget'];
109 | if (!empty($instance['title'])) {
110 | $output .= $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
111 | }
112 | $output .= $table;
113 | $meetings = tsml_get_meetings(['day' => intval(current_time('w')), 'time' => 'upcoming']);
114 | $meetings_link = get_post_type_archive_link('tsml_meeting');
115 | if ($tsml_user_interface == 'tsml_ui' || (!count($meetings) && !empty($instance['message']))) {
116 | $link = $meetings_link;
117 | } else {
118 | $link = $meetings_link . ((strpos($meetings_link, '?') === false) ? '?' : '&') . 'tsml-time=upcoming';
119 | }
120 | $output .= '' . __('View More…', '12-step-meeting-list') . '
';
121 | $output .= $args['after_widget'];
122 |
123 | echo wp_kses($output, [
124 | 'a' => ['href' => [], 'class' => []],
125 | 'aside' => ['class' => []],
126 | 'div' => ['class' => []],
127 | 'h1' => ['class' => []],
128 | 'p' => [],
129 | 'small' => [],
130 | 'span' => ['class' => []],
131 | 'style' => ['type' => []],
132 | 'table' => ['class' => []],
133 | 'tbody' => [],
134 | 'td' => ['class' => []],
135 | 'th' => ['class' => []],
136 | 'thead' => [],
137 | 'tr' => ['class' => []],
138 | ]);
139 | }
140 |
141 | // backend form
142 | public function form($instance)
143 | {
144 | $title = !empty($instance['title']) ? $instance['title'] : __('Upcoming Meetings', '12-step-meeting-list');
145 | $count = !empty($instance['count']) ? $instance['count'] : 5;
146 | $message = !empty($instance['message']) ? $instance['message'] : '';
147 | ?>
148 |
149 |
150 |
151 |
152 |
155 |
156 |
157 |
158 |
159 |
160 |
162 |
163 | >
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
176 |
177 |
178 | >
180 |
181 |
182 |
183 |
184 | !empty($new_instance['title']) ? strip_tags($new_instance['title']) : '',
192 | 'count' => !empty($new_instance['count']) ? intval($new_instance['count']) : 5,
193 | 'css' => !empty($new_instance['css']),
194 | 'message' => !empty($new_instance['message']) ? strip_tags($new_instance['message']) : '',
195 | ];
196 | }
197 | }
198 |
199 | // app store links widget
200 | class TSML_Widget_App_Store extends WP_Widget
201 | {
202 |
203 | // constructor
204 | public function __construct()
205 | {
206 | parent::__construct(
207 | 'tsml_widget_app_store',
208 | __('App Store', '12-step-meeting-list'),
209 | [
210 | 'description' => __('Display links to the Meeting Guide app in the Apple and Android app stores.', '12-step-meeting-list'),
211 | ]
212 | );
213 | }
214 |
215 | // backend form
216 | public function form($instance)
217 | {
218 | $title = empty($instance['title']) ? '' : $instance['title'];
219 | ?>
220 |
221 |
222 |
223 |
224 |
227 |
228 |
229 | >
231 |
232 |
233 |
234 |
235 |
264 | .tsml-widget-app-store {
265 | background-color: transparent;
266 | padding: 0;
267 | }
268 | .tsml-widget-app-store nav {
269 | overflow: auto;
270 | padding: 0;
271 | margin: 0 -7.5px;
272 | }
273 | .tsml-widget-app-store a {
274 | display: inline-block;
275 | width: 50%;
276 | box-sizing: border-box;
277 | padding: 0 7.5px;
278 | float: left;
279 | }
280 | .tsml-widget-app-store img {
281 | width: 100%;
282 | height: auto;
283 | }
284 | #tsml .meetings-widgets h3 {
285 | margin: 0 0 15px;
286 | border-bottom: 1px solid #ddd;
287 | padding: 0 0 10px;
288 | text-align: center;
289 | }
290 | #tsml .meetings-widgets-top .tsml-widget-app-store {
291 | margin: 0 0 15px;
292 | }
293 | #tsml .meetings-widgets-bottom .tsml-widget-app-store {
294 | margin: 30px 0;
295 | }
296 | ';
297 | }
298 |
299 | $output .= '
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | ';
309 |
310 | $output .= $args['after_widget'];
311 |
312 | echo wp_kses_post($output);
313 | }
314 | }
315 |
316 | // register widgets
317 | add_action('widgets_init', function () {
318 | register_widget('TSML_Widget_Upcoming');
319 | register_widget('TSML_Widget_App_Store');
320 | });
321 |
--------------------------------------------------------------------------------
/includes/widgets_init.php:
--------------------------------------------------------------------------------
1 | __('Meetings Top', '12-step-meeting-list'),
7 | 'tsml_meetings_bottom' => __('Meetings Bottom', '12-step-meeting-list'),
8 | 'tsml_meeting_bottom' => __('Meeting Detail Bottom', '12-step-meeting-list'),
9 | 'tsml_location_bottom' => __('Location Detail Bottom', '12-step-meeting-list'),
10 | ];
11 |
12 | foreach ($areas as $id => $name) {
13 | register_sidebar([
14 | 'id' => $id,
15 | 'name' => $name,
16 | 'before_widget' => '',
17 | 'after_widget' => '
',
18 | 'before_title' => '',
19 | 'after_title' => ' ',
20 | ]);
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/assets/css/public.css": "/assets/css/public.css",
3 | "/assets/css/admin.css": "/assets/css/admin.css",
4 | "/assets/css/admin.min.css": "/assets/css/admin.min.css",
5 | "/assets/css/public.min.css": "/assets/css/public.min.css",
6 | "/assets/js/admin.min.js": "/assets/js/admin.min.js",
7 | "/assets/js/public.min.js": "/assets/js/public.min.js"
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "npx mix watch",
5 | "build": "npm run build:mix && npm run build:wp",
6 | "build:mix": "npx mix --production",
7 | "build:wp": "wp-scripts build --webpack-src-dir=assets/src --output-path=assets/build"
8 | },
9 | "devDependencies": {
10 | "@wordpress/babel-preset-default": "^8.11.0",
11 | "@wordpress/scripts": "^30.4.0",
12 | "bootstrap-sass": "^3.4.0",
13 | "laravel-mix": "^6.0.49",
14 | "mapbox-gl": "^2.11.1",
15 | "mark.js": "^8.11.1",
16 | "sass": "^1.56.1",
17 | "sass-loader": "^12.6.0",
18 | "timepicker": "^1.13.15"
19 | },
20 | "overrides": {
21 | "ws": "^8.18.0",
22 | "lighthouse": "^12.1.0",
23 | "puppeteer-core": "^22.13.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 12 Step Meeting List
2 |
3 | This plugin is designed to help recovery programs (AA, NA, Al-Anon, etc) list their meetings. It standardizes addresses, and displays in a list or map.
4 |
5 | The best way to install this plugin is via [its home page](https://wordpress.org/plugins/12-step-meeting-list/) in the WordPress Plugin Directory.
6 |
7 | ## Support
8 |
9 | Have a question? Check out our [Frequently Asked Questions](https://wordpress.org/plugins/12-step-meeting-list/#faq-header).
10 |
11 | Need help? Please [open a new discussion](https://github.com/code4recovery/12-step-meeting-list/discussions).
12 |
13 | ## How can I report security bugs?
14 |
15 | To report a security issue, please use the [Security Tab](https://github.com/code4recovery/12-step-meeting-list/security), located under the repository name. If you cannot see the "Security" tab, select the ... dropdown menu, and then click Security. Please include as much information as possible, including steps to help our team recreate the issue.
16 |
17 | ## Helping with Development
18 |
19 | Do you want to help develop the plugin? We welcome new members! Please find out more at [code4recovery.org](https://code4recovery.org).
20 |
21 | ## Coding Suggestions
22 |
23 | These help improve code readability and maintainability:
24 |
25 | - Use extensions like [DevSense](https://www.devsense.com) and [Prettier](https://prettier.io/) to format code on save
26 | - Use the [Query Monitor WordPress plugin](https://wordpress.org/plugins/query-monitor/) locally to detect and fix any PHP warnings
27 | - All constants, global functions, and global variables should have a name starting with `tsml_`
28 | - Functions ought to be useful in multiple places (except functions that are available to end users such as `tsml_custom_types`)
29 | - Use anonymous functions when possible (we are PHP 5.6+)
30 | - Use bracket syntax for arrays (we are PHP 5.6+)
31 | - We are [PSR-12 compliant](https://www.php-fig.org/psr/psr-12/)
32 |
33 | Also some best practices:
34 |
35 | - Don't leave code commented out (if it's needed later we can find it in the git history)
36 | - Don't put database updates or other expensive operations inside a repeat loop
37 | - No unused variables
38 | - Filter inputs
39 |
40 | ## Compiling Assets
41 |
42 | If you're making changes to JavaScript or CSS, you will want to install SASS and webpack one time by running `npm i`. Then, while developing,
43 | run `npx mix watch` to compile assets as you make changes. When you are ready to make a pull request, run `npx mix --production`.
44 |
45 | ## Rebuilding the POT file
46 |
47 | To support other languages, the plugin wraps output language with:
48 |
49 | ```php
50 | echo __('English message', '12-step-meeting-list')
51 | ```
52 |
53 | To update the `./languages/12-step-meeting-list.pot` file, install [WP Cli](https://make.wordpress.org/cli/handbook/guides/installing/) and run:
54 |
55 | ```bash
56 | wp i18n make-pot . ./languages/12-step-meeting-list.pot --exclude=assets/
57 | ```
58 |
--------------------------------------------------------------------------------
/template.csv:
--------------------------------------------------------------------------------
1 | "Time","End Time","Day","Name","Location","Address","Region","Sub Region","Types","Notes","Location Notes","Timezone","Group","District","Sub District","Website","Website 2","Mailing Address","Venmo","Email","Phone","Group Notes","Contact 1 Name","Contact 1 Email","Contact 1 Phone","Contact 2 Name","Contact 2 Email","Contact 2 Phone","Contact 3 Name","Contact 3 Email","Contact 3 Phone","Last Contact","Conference URL","Conference Phone","Author","Slug","Updated"
2 | "6:00 PM",,"Monday","After Work Topic Meeting","Saturday Nite Live","2634 Union Ave, San Jose, CA 95124, USA","San Jose","West San Jose","Open, Wheelchair Access",,,"America/Los_Angeles","Saturday Nite Live",,,"https://saturdaynitelive.org",,"2634 Union Ave, San Jose, CA 95124, USA","@sample-venmo",,"(800) 555-1212",,,,,,,,,,,,,,,"after-work-topic-meeting","2020-04-0416:50:58"
3 | "6:45 PM",,"Monday","Life on Life's Terms","New Creation Lutheran Church","7275 Santa Teresa Blvd, San Jose, CA 95139, USA","San Jose","South San Jose","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"life-on-lifes-terms","2016-09-1005:15:00"
4 | "7:30 PM",,"Monday","The Shared Gift","St. Timothy's Lutheran Church","5151 Carter Ave, San Jose, CA 95118, USA","San Jose","South San Jose","Closed, Women",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"the-shared-gift","2016-09-1005:15:00"
5 | "8:00 PM",,"Monday","Something Better","2212 Quimby Road","2212 Quimby Rd, San Jose, CA 95122, USA","San Jose","East San Jose","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"something-better","2016-09-1005:15:00"
6 | "8:00 PM",,"Monday","Monday Night Survivors","7511 Gourmet Alley","7511 Gourmet Alley, Gilroy, CA 95020, USA","Gilroy",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"monday-night-survivors","2014-05-3107:32:00"
7 | "8:00 PM",,"Monday","Sobriety Society","Freedom Fellowship: Foothill Covenant Church","1555 Oak Ave, Los Altos, CA 94024, USA","Los Altos",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"sobriety-society","2015-07-2714:10:00"
8 | "8:00 PM",,"Monday","Entire Abstinence","Taiwanese American Presbyterian Church","3675 Payne Ave, San Jose, CA 95117, USA","San Jose","West San Jose","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"entire-abstinence","2016-09-1005:16:00"
9 | "6:30 AM",,"Tuesday","Faith at Work","7511 Gourmet Alley","7511 Gourmet Alley, Gilroy, CA 95020, USA","Gilroy",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"faith-at-work","2014-05-3107:32:00"
10 | "7:00 AM",,"Tuesday","Up the Creek - Daily Reflections","Freedom Fellowship: Foothill Covenant Church","1555 Oak Ave, Los Altos, CA 94024, USA","Los Altos",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"up-the-creek-daily-reflections","2015-05-3004:10:00"
11 | "12:00 PM",,"Tuesday","Open AA","Alano Club West","1555 S 7th St, San Jose, CA 95112, USA","San Jose","Downtown","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"open-aa","2017-02-0900:48:00"
12 | "6:00 PM",,"Tuesday","Men's Spiritual Growth Meeting (Speaker)","7511 Gourmet Alley","7511 Gourmet Alley, Gilroy, CA 95020, USA","Gilroy",,"Closed, Men, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"mens-spiritual-growth-meeting-speaker","2014-09-0504:05:00"
13 | "8:00 PM",,"Tuesday","Step Study","7511 Gourmet Alley","7511 Gourmet Alley, Gilroy, CA 95020, USA","Gilroy",,"Closed, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"step-study","2014-05-3107:32:00"
14 | "8:00 PM",,"Tuesday","Caring & Sharing","Almaden Hills United Methodist Church","1200 Blossom Hill Rd, San Jose, CA 95118, USA","San Jose","South San Jose","Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"caring-sharing","2016-09-1005:15:00"
15 | "12:00 PM",,"Wednesday","Joy of Living","Public Defender's Office","231 Grant Ave, Palo Alto, CA 94306, USA","Palo Alto",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"joy-of-living","2015-05-2700:50:00"
16 | "6:00 PM",,"Wednesday","Progressive Sobriety","Serenity First Fellowship","304 N 6th St, San Jose, CA 95112, USA","San Jose","Downtown","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"progressive-sobriety","2016-09-1005:15:00"
17 | "8:15 PM",,"Wednesday","The Other Wednesday Nite","Denny's Restaurant","1390 S 1st St, San Jose, CA 95110, USA","San Jose","Downtown","Closed, Men, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"the-other-wednesday-nite","2014-05-3107:32:00"
18 | "11:30 AM",,"Thursday","Third Tradition","First Christian Church","80 S 5th St, San Jose, CA 95112, USA","San Jose","Downtown","Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"third-tradition","2016-09-1005:15:00"
19 | "6:00 PM",,"Thursday","Keep It Simple","Cornerstone Fellowship Group","1600 Dell Ave, Campbell, CA 95008, USA","Campbell",,"Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"keep-it-simple","2014-05-3107:32:00"
20 | "9:00 AM",,"Friday","Cup of Coffee Group","Alano Club of San Jose","1122 Fair Ave, San Jose, CA 95122, USA","San Jose","East San Jose","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"cup-of-coffee-group","2016-09-1005:15:00"
21 | "6:00 PM",,"Friday","Better Late Than Never","South County Fellowship","17666 Crest Ave, Morgan Hill, CA 95037, USA","Morgan Hill",,"Birthday, Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"better-late-than-never","2014-05-3107:32:00"
22 | "7:30 PM","8:30 PM","Friday","T.G.I.F.","7511 Gourmet Alley","7511 Gourmet Alley, Gilroy, CA 95020, USA","Gilroy",,"Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"t-g-i-f","2017-01-1802:50:00"
23 | "7:30 PM",,"Friday","Hora de Vivir","Alano Club of San Jose","1122 Fair Ave, San Jose, CA 95122, USA","San Jose","East San Jose","Open, Spanish",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"hora-de-vivir","2016-09-1005:15:00"
24 | "7:30 PM",,"Friday","Gay & Lesbian Step Study & 12 Traditions Group","Billy DeFrank Center","938 The Alameda, San Jose, CA 95126, USA","San Jose","Downtown","Gay, Lesbian, Open, Step Meeting, Tradition Study, Transgender, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"gay-lesbian-step-study-12-traditions-group","2016-09-1005:15:00"
25 | "7:45 PM",,"Friday","Thru the Big Book","Kaiser Permanente San Jose Medical Center","5755 Cottle Rd, San Jose, CA 95123, USA","San Jose","South San Jose","Big Book, Open, Wheelchair Access","This meeting is located in Bldg 3, Rm 2.",,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"thru-the-big-book","2016-09-1005:15:00"
26 | "8:15 PM",,"Friday","Living with Others","Almaden Hills United Methodist Church","1200 Blossom Hill Rd, San Jose, CA 95118, USA","San Jose","South San Jose","Babysitting Available, Birthday, Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"living-with-others","2016-09-1005:15:00"
27 | "7:30 AM",,"Saturday","Early Bird","Denny's Restaurant","1390 S 1st St, San Jose, CA 95110, USA","San Jose","Downtown","Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"early-bird","2014-05-3107:32:00"
28 | "8:30 AM","9:30 AM","Saturday","Cup of Coffee Group","Alano Club of San Jose","1122 Fair Ave, San Jose, CA 95122, USA","San Jose","East San Jose","Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"cup-of-coffee-group-2","2017-02-0719:32:00"
29 | "9:30 AM",,"Saturday","Serenity Saturday","Knights of Columbus","2211 Shamrock Dr, Campbell, CA 95008, USA","Campbell",,"Closed, Women",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"serenity-saturday","2014-05-3107:32:00"
30 | "10:30 AM",,"Saturday","Sobriety on a Dime","Alano Club West","1555 S 7th St, San Jose, CA 95112, USA","San Jose","Downtown","Closed, Women",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"sobriety-on-a-dime","2016-09-1005:15:00"
31 | "6:00 PM",,"Saturday","Saturday Sanctuary","Cornerstone Fellowship Group","1600 Dell Ave, Campbell, CA 95008, USA","Campbell",,"Discussion, Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"saturday-sanctuary","2015-08-0701:03:00"
32 | "6:30 PM",,"Saturday","Gay & Lesbian Topic Discussion","First Presbyterian Church","49 N 4th St, San Jose, CA 95112, USA","San Jose","Downtown","Gay, Lesbian, Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"gay-lesbian-topic-discussion","2016-09-1005:15:00"
33 | "7:15 PM",,"Saturday","Big Book Study","St. Elizabeth's Church","750 Sequoia Dr, Milpitas, CA 95035, USA","Milpitas",,"Big Book, Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"big-book-study","2016-08-0300:43:00"
34 | "8:00 PM",,"Saturday","Positive Outlook","Calvary Church","16330 Los Gatos Blvd, Los Gatos, CA 95032, USA","Los Gatos",,"Birthday, Open",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"positive-outlook","2015-09-0900:44:00"
35 | "9:00 AM",,"Sunday","Spiritual Step Study","Escondido Administration Building","150 Comstock Cir, Stanford, CA 94305, USA","Palo Alto",,"Open, Step Meeting",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"spiritual-step-study","2016-08-0300:32:00"
36 | "6:15 PM",,"Sunday","Serenity Speakers","West Valley Presbyterian Church","6191 Bollinger Rd, Cupertino, CA 95014, USA","Cupertino",,"Open, Wheelchair Access",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,,,,"serenity-speakers","2016-09-0900:23:00"
37 | "7:00 PM",,"Sunday","Underground Book Study","Good Samaritan Episcopal Church","15040 Union Ave, San Jose, CA 95124, USA","San Jose","South San Jose","Big Book, Men, Online Meeting, Open, Temporary Closure",,,"America/Los_Angeles",,,,,,,,,,,,,,,,,,,,,"https://us04web.zoom.us/j/9999999999","(800) 555-1212",,"underground-book-study","2020-04-0416:50:04"
38 |
--------------------------------------------------------------------------------
/templates/archive-tsml-ui.php:
--------------------------------------------------------------------------------
1 | ';
6 |
7 | if (is_active_sidebar('tsml_meetings_top')) { ?>
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 | ';
22 |
23 | tsml_footer();
--------------------------------------------------------------------------------
/templates/footer.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |