├── .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 | 101 | )} 102 | /> 103 | {onlineBackgroundImage && ( 104 | 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 ''; 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 ''; 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 | 45 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 90 | 91 |
81 | 82 |
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 | 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 | 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 | 81 | 82 | 83 | 84 | 85 | 86 | ' . $rows . ' 87 |
' . __('Time', '12-step-meeting-list') . '' . __('Meeting', '12-step-meeting-list') . '' . __('Location', '12-step-meeting-list') . '' . __('Region', '12-step-meeting-list') . '
'; 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 | 152 | 155 |

    156 |

    157 | 160 | 168 |

    169 |

    170 | 173 | 176 |

    177 |

    178 | > 180 | 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 | 224 | 227 |

    228 |

    229 | > 231 | 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 | 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 | 11 | 16 | 19 | '; 22 | 23 | tsml_footer(); -------------------------------------------------------------------------------- /templates/footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/header.php: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | 5 | 6 | 7 | '); 10 | wp_head(); 11 | ?> 12 | 13 | 14 | > 15 | 16 |
    17 | -------------------------------------------------------------------------------- /templates/single-locations.php: -------------------------------------------------------------------------------- 1 | $location->formatted_address, 9 | 'approximate' => $location->approximate, 10 | 'directions' => __('Directions', '12-step-meeting-list'), 11 | 'directions_url' => $location->directions, 12 | 'latitude' => $location->latitude, 13 | 'location' => get_the_title(), 14 | 'location_id' => $location->ID, 15 | 'location_url' => get_permalink($location->ID), 16 | 'longitude' => $location->longitude, 17 | ]); 18 | 19 | // adding custom body classes 20 | add_filter('body_class', function ($classes) { 21 | $classes[] = 'tsml tsml-detail tsml-location'; 22 | return $classes; 23 | }); 24 | 25 | tsml_header(); 26 | ?> 27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 | 45 | 46 |
    47 |
    48 | approximate !== 'yes') { ?> 49 | 62 | 63 | 64 |
    65 |
      66 |
    • 67 |

      68 | formatted_address), TSML_ALLOWED_HTML) ?> 69 |

      70 | 71 | region && !strpos($location->formatted_address, $location->region)) { ?> 72 |

      73 | region) ?> 74 |

      75 | notes) { 78 | tsml_format_notes($location->notes); 79 | } 80 | ?> 81 |
    • 82 | 83 | $location->ID]); 85 | $location_days = []; 86 | foreach ($meetings as $meeting) { 87 | // set types to be empty if it's not given, prevents php notices in log 88 | if (empty($meeting['types'])) { 89 | $meeting['types'] = []; 90 | } 91 | 92 | if (!isset($location_days[$meeting['day']])) { 93 | $location_days[$meeting['day']] = []; 94 | } 95 | 96 | $location_days[$meeting['day']][] = $meeting; 97 | } 98 | ksort($location_days); 99 | 100 | if (count($location_days)) { ?> 101 |
    • 102 | $meetings) { ?> 103 |

      104 | 109 |

      110 |
        111 | 112 |
      • 113 | 114 | 116 | 117 | 118 | 121 |
        122 | () 123 |
        124 | 125 |
        126 | 127 |
        128 |
      • 129 | 130 |
      131 | 132 |
    • 133 | 134 | 135 |
    • 136 | 137 | 138 |
    • 139 |
    140 |
    141 |
    142 |
    143 | 144 |
    145 | 146 |
    147 |
    148 | 149 |
    150 |
    151 | 152 | 153 | 156 | 157 | 158 |
    159 |
    160 |