diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..484371e --- /dev/null +++ b/.babelrc @@ -0,0 +1,33 @@ +{ + "presets": [ + [ + "env", + { + "modules": false, + "targets": { + "browsers": [ + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions", + "last 2 Opera versions", + "last 2 iOS versions", + "last 1 Android version", + "last 1 ChromeAndroid version", + "ie 11", + "> 1%" + ] + } + } + ] + ], + "plugins": [ + "transform-object-rest-spread", + [ + "transform-react-jsx", + { + "pragma": "wp.element.createElement" + } + ] + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2118136 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,164 @@ +{ + "root": true, + "parser": "babel-eslint", + "extends": [ + "wordpress", + "plugin:react/recommended", + "plugin:jsx-a11y/recommended" + ], + "env": { + "browser": false, + "es6": true, + "node": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "globals": { + "TEMPLATE_DIRECTORY": true, + "wp": true, + "wpApiSettings": true, + "window": true, + "document": true + }, + "plugins": [ + "wordpress", + "react", + "jsx-a11y" + ], + "settings": { + "react": { + "pragma": "wp" + } + }, + "rules": { + "array-bracket-spacing": [ "error", "always" ], + "brace-style": [ "error", "1tbs" ], + "camelcase": "off", + "comma-dangle": [ "error", "always-multiline" ], + "comma-spacing": "error", + "comma-style": "error", + "computed-property-spacing": [ "error", "always" ], + "constructor-super": "error", + "dot-notation": "error", + "eol-last": "error", + "eqeqeq": "error", + "func-call-spacing": "error", + "indent": [ "error", "tab", { "SwitchCase": 1 } ], + "jsx-a11y/label-has-for": [ "error", { "required": "id" } ], + "jsx-a11y/media-has-caption": "off", + "jsx-a11y/no-noninteractive-tabindex": "off", + "jsx-a11y/role-has-required-aria-props": "off", + "jsx-quotes": "error", + "key-spacing": "error", + "keyword-spacing": "error", + "lines-around-comment": "off", + "no-alert": "error", + "no-bitwise": "error", + "no-caller": "error", + "no-console": "error", + "no-const-assign": "error", + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-duplicate-imports": "error", + "no-else-return": "error", + "no-eval": "error", + "no-extra-semi": "error", + "no-fallthrough": "error", + "no-lonely-if": "error", + "no-mixed-operators": "error", + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": [ "error", { "max": 1 } ], + "no-multi-spaces": "error", + "no-multi-str": "off", + "no-negated-in-lhs": "error", + "no-nested-ternary": "error", + "no-redeclare": "error", + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])", + "message": "Translate function arguments must be string literals." + }, + { + "selector": "CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])", + "message": "Translate function arguments must be string literals." + }, + { + "selector": "CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])", + "message": "Translate function arguments must be string literals." + }, + { + "selector": "CallExpression[callee.name='__'][arguments.length!=2]", + "message": "Translate function __ must always be invoked with two arguments." + }, + { + "selector": "CallExpression[callee.name='_x'][arguments.length!=3]", + "message": "Translate function _x must always be invoked with three arguments." + }, + { + "selector": "CallExpression[callee.name='_n'][arguments.length!=4]", + "message": "Translate function _n must always be invoked with four arguments." + }, + { + "selector": "CallExpression[callee.name='_nx'][arguments.length!=5]", + "message": "Translate function _nx must always be invoked with five arguments." + } + ], + "no-shadow": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-unreachable": "error", + "no-unsafe-negation": "error", + "no-unused-expressions": "error", + "no-unused-vars": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-return": "error", + "no-var": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ "error", "always" ], + "padded-blocks": [ "error", "never" ], + "prefer-const": "error", + "quote-props": [ "error", "as-needed" ], + "react/display-name": "off", + "react/jsx-curly-spacing": [ + "error", + { + "when": "always", + "children": true + } + ], + "react/jsx-equals-spacing": "error", + "react/jsx-indent": [ "error", "tab" ], + "react/jsx-indent-props": [ "error", "tab" ], + "react/jsx-key": "error", + "react/jsx-tag-spacing": "error", + "react/no-children-prop": "off", + "react/no-find-dom-node": "warn", + "react/prop-types": "off", + "semi": "error", + "semi-spacing": "error", + "space-before-blocks": [ "error", "always" ], + "space-before-function-paren": [ "error", "never" ], + "space-in-parens": [ "error", "always" ], + "space-infix-ops": [ "error", { "int32Hint": false } ], + "space-unary-ops": [ + "error", + { + "overrides": { + "!": true + } + } + ], + "template-curly-spacing": [ "error", "always" ], + "valid-typeof": "error", + "yoda": "off" + } +} diff --git a/.gitignore b/.gitignore index 81628f8..0ecf306 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ +# Directories/files used for wp.org .svn screenshot-1.png screenshot-2.png + +# Directories/files that may be generated by this project +node_modules +blocks/build + +# Directories/files that may appear in your environment +.DS_Store +*.log +phpcs.xml +yarn.lock +package-lock.json +mock-data +build + +/vendor/ +composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..854b97b --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "ryelle/meetup-widgets", + "type": "project", + "require": { + "php": ">=5.6", + "xamin/handlebars.php": "dev-master" + }, + "license": "GPL-2.0+", + "authors": [ + { + "name": "Kelly Dwan" + } + ], + "minimum-stability": "stable" +} diff --git a/includes/api/README.md b/includes/api/README.md new file mode 100644 index 0000000..42cf4d8 --- /dev/null +++ b/includes/api/README.md @@ -0,0 +1,26 @@ +API +=== + +This folder contains controllers for Meetup.com proxy endpoints, using the API key set in wp-admin. These endpoints are each accessible by `GET` requests only, and only by logged-in users. + +### Event Lists + + meetup/v1/events/self + +This lists upcoming events for the user who created the API key. + + meetup/v1/events/(?P[^/]+) + +This lists upcoming events for the group passed through. This is the group `urlname`, for example, if the URL for your meetup is `https://www.meetup.com/boston-wordpress-meetup/`, the `urlname` is `boston-wordpress-meetup`. + +### Single Event + + meetup/v1/events/(?P[^/]+)/(?P[^/]+) + +This gets information about a single event in a group. See above for the `group_urlname` description. The event ID is the part after `/events/` in a URL for a single event. + +### Groups List + + meetup/v1/groups/self + +This lists the groups that the API key owner is a member of. diff --git a/includes/api/class-meetup-rest-events-controller.php b/includes/api/class-meetup-rest-events-controller.php new file mode 100644 index 0000000..d5ac2a7 --- /dev/null +++ b/includes/api/class-meetup-rest-events-controller.php @@ -0,0 +1,223 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_endpoint_args(), + ), + ) ); + + register_rest_route( $namespace, '/' . $base . '/(?P[^/]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_endpoint_args(), + ), + ) ); + + register_rest_route( $namespace, '/' . $base . '/(?P[^/]+)/(?P[^/]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + ) ); + } + + /** + * Get a collection of items + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $api = new Meetup_API_V3(); + $params = $request->get_params(); + $per_page = intval( $params['per_page'] ); + $args = array( + 'status' => 'upcoming', + 'page' => $per_page, + ); + + if ( isset( $params['id'] ) ) { + $id = $params['id']; + $items = $api->get_events( $id, $args, 'vsm_v3_group_' . $id . '_' . $per_page ); + } else { + $items = $api->get_self_events( $args, 'vsm_v3_self_' . $per_page ); + } + + if ( ! $items ) { + return []; + } + if ( is_wp_error( $items ) ) { + return $items; + } + + $data = array(); + foreach ( $items as $item ) { + $itemdata = $this->prepare_item_for_response( $item, $request ); + $data[] = $this->prepare_response_for_collection( $itemdata ); + } + + return new WP_REST_Response( $data, 200 ); + } + + /** + * Get one item from the collection + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $api = new Meetup_API_V3(); + $params = $request->get_params(); + $item = $api->get_event( + $params['group_id'], + $params['event_id'], + 'vsm_v3_event_' . $params['group_id'] . '_' . $params['event_id'] + ); + if ( ! $item ) { + return []; + } + if ( is_wp_error( $item ) ) { + return $item; + } + + $data = $this->prepare_item_for_response( $item, $request ); + + // return a response or error based on some conditional + return new WP_REST_Response( $data, 200 ); + } + + /** + * Prepare the item for the REST response + * + * @param mixed $item Meetup.com representation of the event. + * @param WP_REST_Request $request Request object. + * @return mixed + */ + public function prepare_item_for_response( $item, $request ) { + $venue_defaults = array( + 'name' => '', + 'address_1' => '', + 'address_2' => '', + 'address_3' => '', + 'city' => '', + 'state' => '', + 'country' => '', + ); + if ( isset( $item->venue ) ) { + $venue = wp_parse_args( + (array) $item->venue, + $venue_defaults + ); + $venue_str = sprintf( + '%1$s – %2$s, %3$s, %4$s', + $venue['name'], + $venue['address_1'], + $venue['city'], + $venue['state'] + ); + } else { + $venue = $venue_defaults; + $venue_str = ''; + } + + $description = ''; + $excerpt = ''; + if ( isset( $item->description ) ) { + $description = $item->description; + $excerpt = wp_trim_words( $item->description, 40, '…' ); + } + + $time_seconds = intval( $item->time / 1000 + $item->utc_offset / 1000 ); + + return array( + 'id' => $item->id, + 'name' => $item->name, + 'description' => $description, + 'excerpt' => $excerpt, + 'url' => $item->link, + 'google_maps' => "http://maps.google.com/maps?q={$venue_str}&z=17", + 'date' => date( 'M d, g:ia', $time_seconds ), + 'long_date' => date( 'l, F d, Y g:ia', $time_seconds ), // Thursday, December 14, 2017 6:30 PM + 'datetime' => date( 'c', $time_seconds ), // 2017-03-19T04:39:44+00:00 + 'raw_date' => $item->time, + 'status' => $item->status, + 'venue' => $venue, + 'venue_display' => $venue_str, + 'yes_rsvp_count' => $item->yes_rsvp_count, + ); + } + + /** + * Check permissions for this endpoint. + * + * Only logged-in users can use this proxy, to prevent anonymous users from + * spamming the meetup.com API with the site-owner's API key. The exception + * to this is requests from the server itself, which are ID'd by having the + * nonce `X-MW-Nonce`. This is not revealed client-side, so is safe to use + * as a "security" measure. + * + * @param WP_REST_Request $request Current request. + */ + public function get_items_permissions_check( $request ) { + if ( wp_verify_nonce( $request->get_header( 'x-mw-nonce' ), 'meetup-widgets' ) ) { + return true; + } + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( + 'rest_forbidden', + esc_html__( 'You cannot view the event resource.', 'meetup-widgets' ), + array( + 'status' => is_user_logged_in() ? 403 : 401, + ) + ); + } + return true; + } + + /** + * Get the argument schema for this example endpoint. + */ + function get_endpoint_args() { + $args = array(); + + // Here we add our PHP representation of JSON Schema. + $args['count'] = array( + 'description' => esc_html__( 'Number of events to show.', 'meetup-widgets' ), + 'type' => 'integer', + 'validate_callback' => 'absint', + 'sanitize_callback' => 'absint', + 'required' => false, + 'default' => 3, + ); + + return $args; + } +} diff --git a/includes/api/class-meetup-rest-groups-controller.php b/includes/api/class-meetup-rest-groups-controller.php new file mode 100644 index 0000000..99accef --- /dev/null +++ b/includes/api/class-meetup-rest-groups-controller.php @@ -0,0 +1,101 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + ) ); + } + + /** + * Get a collection of items + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $api = new Meetup_API_V3(); + $items = $api->get_self_groups( 'vsm_v3_self_groups' ); + + if ( ! $items ) { + return []; + } + if ( is_wp_error( $items ) ) { + return $items; + } + + $data = array(); + foreach ( $items as $item ) { + $itemdata = $this->prepare_item_for_response( $item, $request ); + $data[] = $this->prepare_response_for_collection( $itemdata ); + } + + return new WP_REST_Response( $data, 200 ); + } + + /** + * Prepare the item for the REST response + * + * @param mixed $item Meetup.com representation of the group. + * @param WP_REST_Request $request Request object. + * @return mixed + */ + public function prepare_item_for_response( $item, $request ) { + return array( + 'id' => $item->id, + 'name' => $item->name, + 'urlname' => $item->urlname, + ); + } + + /** + * Check permissions for this endpoint. + * + * Only logged-in users can use this proxy, to prevent anonymous users from + * spamming the meetup.com API with the site-owner's API key. The exception + * to this is requests from the server itself, which are ID'd by having the + * nonce `X-MW-Nonce`. This is not revealed client-side, so is safe to use + * as a "security" measure. + * + * @param WP_REST_Request $request Current request. + */ + public function get_items_permissions_check( $request ) { + if ( wp_verify_nonce( $request->get_header( 'x-mw-nonce' ), 'meetup-widgets' ) ) { + return true; + } + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( + 'rest_forbidden', + esc_html__( 'You cannot view the group resource.', 'meetup-widgets' ), + array( + 'status' => is_user_logged_in() ? 403 : 401, + ) + ); + } + return true; + } +} diff --git a/includes/blocks/README.md b/includes/blocks/README.md new file mode 100644 index 0000000..441fc8c --- /dev/null +++ b/includes/blocks/README.md @@ -0,0 +1,35 @@ +Blocks +====== + +Meetup-Widgets currently supports two blocks: a list of upcoming events by group, and a list of upcoming events for the API key owner. + +## Project Setup + +To work on the meetup-widgets blocks, you need node, npm & composer installed. After making sure those are installed, install the project's dependencies: + + composer install + npm install + +If you have `WP_DEBUG` set to true, the plugin looks for the files on the webpack dev server, so you'll need to start it: + + npm start + +If you have `WP_DEBUG` set to false, you can just build the files directly: + + npm run build + +## Development + +The editing interface for both blocks lives in `blocks/src/components`. The content returned from the component's `render` function is presented in the editor. This content is _not_ saved to the database. Instead, WordPress calls the PHP render functions in `includes/class-meetup-widgets-blocks.php` for each block. This guarantees up-to-date event results, because the data is queried every page load (with a 2hr API cache). + +### Handlebars + +Since the data is displayed in both JS (when editing) and PHP (when viewing), I've extracted the templates out into handlebars. + +The javascript side uses [handlebars-loader](https://www.npmjs.com/package/handlebars-loader) to pre-compile the template into a function that accepts the event & attribute data & returns HTML. This needs to be "dangerously set" into the react component. + +The PHP side uses [handlebars.php](https://github.com/XaminProject/handlebars.php), which is installed via composer. There are quirks with handlebars when used in this way, some nested `if/each` sections don't behave as expected. This is why the if/else sections are structured as they are. + +### CSS + +The CSS is built from `style.css`, and is currently very minimal. This is loaded in the gutenberg editor, and on the frontend of the site. diff --git a/includes/blocks/src/components/group-list.js b/includes/blocks/src/components/group-list.js new file mode 100644 index 0000000..2a8beed --- /dev/null +++ b/includes/blocks/src/components/group-list.js @@ -0,0 +1,157 @@ +/** @format */ +/** + * External Dependencies + */ +import { stringify } from 'qs'; + +/** + * Internal Dependencies + */ +const runTemplate = require( TEMPLATE_DIRECTORY + '/meetup-list.hbs' ); + +/** + * Core WP Dependencies + */ +const { __ } = wp.i18n; +const { Component } = wp.element; +const { RichText, InspectorControls } = wp.editor; +const { + Dashicon, + Placeholder, + PanelBody, + RangeControl, + Spinner, + SelectControl, + TextControl, + ToggleControl, + withAPIData, +} = wp.components; + +class GroupListBlock extends Component { + constructor() { + super( ...arguments ); + this.onChangeEditable = this.onChangeEditable.bind( this ); + this.onChangeToggle = this.onChangeToggle.bind( this ); + this.renderEventsList = this.renderEventsList.bind( this ); + } + + onChangeEditable( field ) { + return value => this.props.setAttributes( { [ field ]: value } ); + } + + onChangeToggle( field ) { + return () => + this.props.setAttributes( { + [ field ]: ! this.props.attributes[ field ], + } ); + } + + renderEventsList() { + const { attributes, events = {} } = this.props; + const { isLoading, error, data = [] } = events; + + /* eslint-disable yoda */ + if ( error && error.status > 200 ) { + let message = __( 'There was an error loading the API for this block', 'meetup-widgets' ); + if ( error.resposeJSON && error.resposeJSON.message ) { + message = error.resposeJSON.message; + } + + return ( + + + + ); + } + /* eslint-enable yoda */ + + if ( isLoading ) { + return ( + + + + ); + } + + const vars = { + attributes, + events: data, + hide_title: true, // title is editable here, so we hide it in the final template. + show_events: !! data.length, + show_events_description: !! data.length && attributes.show_description, + }; + + return { __html: runTemplate( vars ) }; + } + + render() { + const { attributes, isSelected, groups: { data = [] } } = this.props; + + const groupOptions = data.map( group => ( { + label: group.name, + value: group.urlname, + } ) ); + groupOptions.unshift( { + label: __( 'Select a group…', 'meetup-widgets' ), + value: '', + } ); + + const controls = ( + + + + + + + + + ); + + const list = this.renderEventsList(); + const { title } = attributes; + + return [ + controls, +
+ { ( ( title && title.length > 0 ) || isSelected ) && ( + + ) } + { list.__html ?
: list } +
, + ]; + } +} + +export default withAPIData( props => { + const { group, per_page = 3 } = props.attributes; + const queryString = stringify( { per_page } ); + return { + events: group ? `/meetup/v1/events/${ group }?${ queryString }` : {}, + groups: '/meetup/v1/groups/self', + }; +} )( GroupListBlock ); diff --git a/includes/blocks/src/components/user-list.js b/includes/blocks/src/components/user-list.js new file mode 100644 index 0000000..f09646c --- /dev/null +++ b/includes/blocks/src/components/user-list.js @@ -0,0 +1,140 @@ +/** @format */ +/** + * External Dependencies + */ +import { stringify } from 'qs'; + +/** + * Internal Dependencies + */ +const runTemplate = require( TEMPLATE_DIRECTORY + '/meetup-list.hbs' ); + +/** + * Core WP Dependencies + */ +const { __ } = wp.i18n; +const { Component } = wp.element; +const { RichText, InspectorControls } = wp.editor; +const { + Dashicon, + Placeholder, + PanelBody, + RangeControl, + Spinner, + TextControl, + ToggleControl, + withAPIData, +} = wp.components; + +class UserListBlock extends Component { + constructor() { + super( ...arguments ); + this.onChangeEditable = this.onChangeEditable.bind( this ); + this.onChangeToggle = this.onChangeToggle.bind( this ); + this.renderEventsList = this.renderEventsList.bind( this ); + } + + onChangeEditable( field ) { + return value => this.props.setAttributes( { [ field ]: value } ); + } + + onChangeToggle( field ) { + return () => + this.props.setAttributes( { + [ field ]: ! this.props.attributes[ field ], + } ); + } + + renderEventsList() { + const { attributes, events = {} } = this.props; + const { isLoading, error, data = [] } = events; + + /* eslint-disable yoda */ + if ( error && error.status > 200 ) { + let message = __( 'There was an error loading the API for this block', 'meetup-widgets' ); + if ( error.resposeJSON && error.resposeJSON.message ) { + message = error.resposeJSON.message; + } + + return ( + + + + ); + } + /* eslint-enable yoda */ + + if ( isLoading ) { + return ( + + + + ); + } + + const vars = { + attributes, + events: data, + hide_title: true, // title is editable here, so we hide it in the final template. + show_events: !! data.length, + show_events_description: !! data.length && attributes.show_description, + }; + + return { __html: runTemplate( vars ) }; + } + + render() { + const { attributes, isSelected } = this.props; + + const controls = ( + + + + + + + + ); + + const list = this.renderEventsList(); + const { title } = attributes; + + return [ + controls, +
+ { ( ( title && title.length > 0 ) || isSelected ) && ( + + ) } + { list.__html ?
: list } +
, + ]; + } +} + +export default withAPIData( props => { + const { per_page = 3 } = props.attributes; + const queryString = stringify( { per_page } ); + return { + events: `/meetup/v1/events/self?${ queryString }`, + }; +} )( UserListBlock ); diff --git a/includes/blocks/src/group-list.js b/includes/blocks/src/group-list.js new file mode 100755 index 0000000..ccf58cd --- /dev/null +++ b/includes/blocks/src/group-list.js @@ -0,0 +1,27 @@ +/** @format */ +/** + * Core WP Dependencies + */ +const { __ } = wp.i18n; + +/** + * Internal Dependencies + */ +import GroupListBlock from './components/group-list'; + +// Visit https://wordpress.org/gutenberg/handbook/block-api/ to learn about Block API +export default { + title: __( 'Meetup.com List', 'meetup-widgets' ), + description: __( + 'This is a list of events for a given group on Meetup.com', + 'meetup-widgets' + ), + icon: 'groups', + category: 'embed', + supports: { + anchor: true, + html: false, + }, + edit: GroupListBlock, + save: () => null, +}; diff --git a/includes/blocks/src/index.js b/includes/blocks/src/index.js new file mode 100644 index 0000000..3943093 --- /dev/null +++ b/includes/blocks/src/index.js @@ -0,0 +1,10 @@ +/** @format */ + +const { registerBlockType } = wp.blocks; +import userListOptions from './user-list'; +import groupListOptions from './group-list'; + +import './style.css'; + +registerBlockType( 'meetup-widgets/user-list', userListOptions ); +registerBlockType( 'meetup-widgets/group-list', groupListOptions ); diff --git a/includes/blocks/src/style.css b/includes/blocks/src/style.css new file mode 100644 index 0000000..065ede3 --- /dev/null +++ b/includes/blocks/src/style.css @@ -0,0 +1,7 @@ +/** @format */ + +.meetup-events__event-date { + display: block; + color: #6c7781; + font-size: 13px; +} diff --git a/includes/blocks/src/user-list.js b/includes/blocks/src/user-list.js new file mode 100644 index 0000000..ade7da6 --- /dev/null +++ b/includes/blocks/src/user-list.js @@ -0,0 +1,27 @@ +/** @format */ +/** + * Core WP Dependencies + */ +const { __ } = wp.i18n; + +/** + * Internal Dependencies + */ +import UserListBlock from './components/user-list'; + +// Visit https://wordpress.org/gutenberg/handbook/block-api/ to learn about Block API +export default { + title: __( 'Meetup.com User List', 'meetup-widgets' ), + description: __( + 'This is a list of the upcoming events on Meetup.com for the user that created the API key', + 'meetup-widgets' + ), + icon: 'groups', + category: 'embed', + supports: { + anchor: true, + html: false, + }, + edit: UserListBlock, + save: () => null, +}; diff --git a/includes/class-meetup-api-v3.php b/includes/class-meetup-api-v3.php new file mode 100644 index 0000000..664039e --- /dev/null +++ b/includes/class-meetup-api-v3.php @@ -0,0 +1,221 @@ +api_key = $options['vs_meetup_api_key']; + } + + /** + * Given arguments & a transient name, grab data from the events API + * + * @param string $group Group name used to fetch events. + * @param array $args Query params to send to events call. + * @param string $transient The transient name (if empty, no transient stored). + * @return array Event data (list of events) + */ + public function get_events( $group = false, $args = [], $transient = '' ) { + if ( ! $group ) { + return new WP_Error( 'undefined_group', __( 'Requested group name missing.', 'meetup-widgets' ) ); + } + $data = false; + if ( $transient ) { + $data = get_transient( $transient ); + } + if ( empty( $this->api_key ) ) { + return new WP_Error( 'undefined_key', __( 'Please enter an API key.', 'meetup-widgets' ) ); + } + + $defaults = array( + 'key' => $this->api_key, + ); + + if ( false === $data ) { + $args = wp_parse_args( $args, $defaults ); + $url = sprintf( '%s/%s/events', $this->base_url, $group ); + $url = add_query_arg( $args, $url ); + $events_response = wp_remote_get( $url ); + if ( is_wp_error( $events_response ) ) { + return $events_response; + } + $data = json_decode( $events_response['body'] ); + if ( isset( $data->errors ) ) { + $err = array_shift( $data->errors ); + return new WP_Error( $err->code, $err->message ); + } + if ( ! is_array( $data ) ) { + return new WP_Error( 'response_error', __( 'Response is not formatted correctly', 'meetup-widgets' ) ); + } + + if ( $transient ) { + set_transient( $transient, $data, 2 * HOUR_IN_SECONDS ); + } + } + + return $data; + } + + /** + * Given arguments & a transient name, grab data from the events API + * + * @param array $args Query params to send to events call. + * @param string $transient The transient name (if empty, no transient stored). + * @return array Event data (list of events) + */ + public function get_self_events( $args = [], $transient = '' ) { + $data = false; + if ( $transient ) { + $data = get_transient( $transient ); + } + if ( empty( $this->api_key ) ) { + return new WP_Error( 'undefined_key', __( 'Please enter an API key.', 'meetup-widgets' ) ); + } + + $defaults = array( + 'key' => $this->api_key, + ); + + if ( false === $data ) { + $args = wp_parse_args( $args, $defaults ); + $url = sprintf( '%s/self/events', $this->base_url ); + $url = add_query_arg( $args, $url ); + $events_response = wp_remote_get( $url ); + if ( is_wp_error( $events_response ) ) { + return $events_response; + } + $data = json_decode( $events_response['body'] ); + if ( isset( $data->errors ) ) { + $err = array_shift( $data->errors ); + return new WP_Error( $err->code, $err->message ); + } + if ( ! is_array( $data ) ) { + return new WP_Error( 'response_error', __( 'Response is not formatted correctly', 'meetup-widgets' ) ); + } + + if ( $transient ) { + set_transient( $transient, $data, 2 * HOUR_IN_SECONDS ); + } + } + + return $data; + } + + /** + * Given arguments & a transient name, grab data from the events API + * + * @param string $group The parent group name. + * @param string $event The event ID to fetch. + * @param string $transient The transient name (if empty, no transient stored). + * @return array Event data (single event) + */ + public function get_event( $group = false, $event = false, $transient = '' ) { + if ( ! $group ) { + return new WP_Error( 'undefined_group', __( 'Requested group name missing.', 'meetup-widgets' ) ); + } + if ( ! $event ) { + return new WP_Error( 'undefined_event', __( 'Requested event ID missing.', 'meetup-widgets' ) ); + } + if ( empty( $this->api_key ) ) { + return new WP_Error( 'undefined_key', __( 'Please enter an API key.', 'meetup-widgets' ) ); + } + + $data = false; + if ( $transient ) { + $data = get_transient( $transient ); + } + + $args = array( + 'key' => $this->api_key, + ); + + if ( false === $data ) { + $url = sprintf( '%s/%s/events/%s', $this->base_url, $group, $event ); + $url = add_query_arg( $args, $url ); + $event_response = wp_remote_get( $url ); + if ( is_wp_error( $event_response ) ) { + return $event_response; + } + $data = json_decode( $event_response['body'] ); + if ( isset( $data->errors ) ) { + $err = array_shift( $data->errors ); + return new WP_Error( $err->code, $err->message ); + } + + if ( $transient ) { + set_transient( $transient, $data, 2 * HOUR_IN_SECONDS ); + } + } + + return $data; + } + + /** + * Given arguments & a transient name, grab data from the groups API + * + * @param string $transient The transient name (if empty, no transient stored). + * @return array Event data (list of events) + */ + public function get_self_groups( $transient = '' ) { + $data = false; + if ( $transient ) { + $data = get_transient( $transient ); + } + if ( empty( $this->api_key ) ) { + return new WP_Error( 'undefined_key', __( 'Please enter an API key.', 'meetup-widgets' ) ); + } + + $args = array( + 'key' => $this->api_key, + 'page' => 200, + ); + + if ( false === $data ) { + $url = sprintf( '%s/self/groups', $this->base_url ); + $url = add_query_arg( $args, $url ); + $groups_response = wp_remote_get( $url ); + if ( is_wp_error( $groups_response ) ) { + return $groups_response; + } + $data = json_decode( $groups_response['body'] ); + if ( isset( $data->errors ) ) { + $err = array_shift( $data->errors ); + return new WP_Error( $err->code, $err->message ); + } + if ( ! is_array( $data ) ) { + return new WP_Error( 'response_error', __( 'Response is not formatted correctly', 'meetup-widgets' ) ); + } + + if ( $transient ) { + set_transient( $transient, $data, 2 * HOUR_IN_SECONDS ); + } + } + + return $data; + } + +} diff --git a/class-meetup-widget.php b/includes/class-meetup-widget.php similarity index 100% rename from class-meetup-widget.php rename to includes/class-meetup-widget.php diff --git a/class-meetup-widgets-admin.php b/includes/class-meetup-widgets-admin.php similarity index 100% rename from class-meetup-widgets-admin.php rename to includes/class-meetup-widgets-admin.php diff --git a/includes/class-meetup-widgets-blocks.php b/includes/class-meetup-widgets-blocks.php new file mode 100755 index 0000000..3b443be --- /dev/null +++ b/includes/class-meetup-widgets-blocks.php @@ -0,0 +1,215 @@ +dir = dirname( dirname( __FILE__ ) ); + + // Check for Gutenberg + if ( ! function_exists( 'register_block_type' ) ) { + // No Gutenberg 😭 + return; + } + + $this->register_block_assets(); + + register_block_type( 'meetup-widgets/group-list', array( + 'attributes' => array( + 'title' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string' + ), + ), + 'placeholder' => array( + 'type' => 'string', + 'default' => __( 'No upcoming events.', 'meetup-widgets' ), + ), + 'group' => array( + 'type' => 'string', + ), + 'per_page' => array( + 'type' => 'number', + 'default' => 5, + ), + 'show_description' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + 'render_callback' => array( $this, 'render_block_group_list' ), + 'editor_script' => 'meetup-blocks', + 'editor_style' => 'meetup-blocks', + ) ); + + register_block_type( 'meetup-widgets/user-list', array( + 'attributes' => array( + 'title' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string' + ), + ), + 'placeholder' => array( + 'type' => 'string', + 'default' => __( 'No upcoming events.', 'meetup-widgets' ), + ), + 'per_page' => array( + 'type' => 'number', + 'default' => 5, + ), + 'show_description' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + 'render_callback' => array( $this, 'render_block_user_list' ), + 'editor_script' => 'meetup-blocks', + 'editor_style' => 'meetup-blocks', + ) ); + + add_action( 'wp_enqueue_scripts', function() { + wp_enqueue_style( 'meetup-blocks' ); + } ); + } + + /** + * Add Gutenberg block JS & CSS to the editor + */ + public function register_block_assets() { + $js_file = 'http://localhost:8081/build/index.js'; + $css_file = 'http://localhost:8081/build/editor.css'; + $js_version = false; + $css_version = false; + + if ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) { + $js_file = plugins_url( 'build/index.js', dirname( __FILE__ ) ); + $css_file = plugins_url( 'build/editor.css', dirname( __FILE__ ) ); + $js_version = filemtime( "{$this->dir}/build/index.js" ); + $css_version = filemtime( "{$this->dir}/build/editor.css" ); + } + + wp_register_script( 'meetup-blocks', $js_file, [ 'wp-blocks', 'wp-i18n', 'wp-element' ], $js_version ); + wp_register_style( 'meetup-blocks', $css_file, [ 'wp-blocks' ], $css_version ); + } + + /** + * Renders the `meetup-widgets/group-list` block on server. + * + * @param array $attributes { + * The block attributes. + * @type string $title Title to display. + * @type string $group URL name of the group. + * @type int $per_page Number of events to display. + * @type bool $show_description Show the description in the list. Also switches to a more + * verbose display of event data. + * @type string $placeholder A placeholder shown when there are no events. + * } + * @return string Returns the upcoming events for a given group, to be displayed in a post + */ + public static function render_block_group_list( $attributes ) { + // Initialize handlebars + $loader = new \Handlebars\Loader\FilesystemLoader( + VSMEET_TEMPLATE_DIR, + array( + 'extension' => 'hbs', + ) + ); + $engine = new Handlebars( array( + 'loader' => $loader, + ) ); + + $request = new WP_REST_Request( 'GET', '/meetup/v1/events/' . $attributes['group'] ); + $request->set_header( 'x-mw-nonce', wp_create_nonce( 'meetup-widgets' ) ); + $request->set_query_params( array( + 'per_page' => $attributes['per_page'], + ) ); + $response = rest_do_request( $request ); + $events = $response->get_data(); + + $has_events = ! isset( $events['code'] ) && count( $events ) > 0; + + if ( is_array( $attributes['title'] ) ) { + $attributes['title'] = join( ' ', $attributes['title'] ); + } + $vars = [ + 'attributes' => $attributes, + 'events' => $events, + 'hide_title' => empty( $attributes['title'] ), + 'show_events' => $has_events, + 'show_events_description' => $has_events && $attributes['show_description'], + ]; + $output = $engine->render( 'meetup-list', $vars ); + + return $output; + } + + /** + * Renders the `meetup-widgets/user-list` block on server. + * + * @param array $attributes { + * The block attributes. + * @type string $title Title to display. + * @type int $per_page Number of events to display. + * @type bool $show_description Show the description in the list. Also switches to a more + * verbose display of event data. + * @type string $placeholder A placeholder shown when there are no events. + * } + * @return string Returns the upcoming events for the current API user, to be displayed in a post + */ + public static function render_block_user_list( $attributes ) { + // Initialize handlebars + $loader = new \Handlebars\Loader\FilesystemLoader( + VSMEET_TEMPLATE_DIR, + array( + 'extension' => 'hbs', + ) + ); + $engine = new Handlebars( array( + 'loader' => $loader, + ) ); + + $request = new WP_REST_Request( 'GET', '/meetup/v1/events/self' ); + $request->set_header( 'x-mw-nonce', wp_create_nonce( 'meetup-widgets' ) ); + $request->set_query_params( array( + 'per_page' => $attributes['per_page'], + ) ); + $response = rest_do_request( $request ); + $events = $response->get_data(); + + $has_events = count( $events ) > 0; + + if ( is_array( $attributes['title'] ) ) { + $attributes['title'] = join( ' ', $attributes['title'] ); + } + $vars = [ + 'attributes' => $attributes, + 'events' => $events, + 'hide_title' => false, + 'show_events' => $has_events, + 'show_events_description' => $has_events && $attributes['show_description'], + ]; + $output = $engine->render( 'meetup-list', $vars ); + + return $output; + } +} diff --git a/includes/templates/meetup-list.hbs b/includes/templates/meetup-list.hbs new file mode 100644 index 0000000..a15ba70 --- /dev/null +++ b/includes/templates/meetup-list.hbs @@ -0,0 +1,33 @@ +
+ {{#unless hide_title}} +

{{attributes.title}}

+ {{/unless}} + {{#if show_events_description}} + {{#each events}} +
+

+ {{this.name}} +

+ +

{{this.excerpt}}

+
+ {{/each}} + {{else}} + {{#if show_events}} +
    + {{#each events}} +
  • + {{this.name}} + +
  • + {{/each}} +
+ {{else}} + {{#if events.code }} +

{{events.message}}

+ {{else}} +

{{attributes.placeholder}}

+ {{/if}} + {{/if}} + {{/if}} +
diff --git a/templates/meetup-single.php b/includes/templates/meetup-single.php similarity index 100% rename from templates/meetup-single.php rename to includes/templates/meetup-single.php diff --git a/includes/widgets/group-list.php b/includes/widgets/group-list.php new file mode 100644 index 0000000..aaeea0c --- /dev/null +++ b/includes/widgets/group-list.php @@ -0,0 +1,148 @@ + 'widget_meetup_group_list', + 'description' => __( 'Display a list of events.', 'meetup-widgets' ), + ) + ); + } + + /** + * Normalize and sanitize the widget attribute values + */ + function get_sanitized_values( $attrs ) { + return array( + 'title' => isset( $attrs['title'] ) ? strip_tags( $attrs['title'] ) : '', + 'id' => isset( $attrs['id'] ) ? sanitize_title( $attrs['id'] ) : '', + 'limit' => isset( $attrs['limit'] ) ? filter_var( $attrs['limit'], FILTER_VALIDATE_INT ) : 3, + 'show_desc' => isset( $attrs['show_desc'] ) ? filter_var( $attrs['show_desc'], FILTER_VALIDATE_BOOLEAN ) : false, + ); + } + + /** + * Display the widget content + * + * @see WP_Widget::widget + */ + function widget( $args, $instance ) { + $attrs = $this->get_sanitized_values( $instance ); + $title = apply_filters( 'widget_title', $attrs['title'] ); + + echo $args['before_widget']; + if ( $title ) { + echo $args['before_title'] . $title . $args['after_title']; + } + echo Meetup_Widgets_Blocks::render_block_group_list( array( + 'title' => '', + 'group' => $attrs['id'], + 'per_page' => $attrs['limit'], + 'show_description' => $attrs['show_desc'], + 'placeholder' => __( 'No upcoming events.', 'meetup-widgets' ), + ) ); + echo $args['after_widget']; + } + + /** + * Save the widget settings + * + * @see WP_Widget::update + */ + function update( $new_instance, $old_instance ) { + $new_values = $this->get_sanitized_values( $new_instance ); + return array_merge( $old_instance, $new_values ); + } + + /** + * Display the widget settings form + * + * @see WP_Widget::form + */ + function form( $instance ) { + $attrs = $this->get_sanitized_values( $instance ); + $groups = $this->get_groups_from_api(); + ?> +

+ + +

+

+ + +

+

+ + +

+

+ +

+ set_header( 'x-mw-nonce', wp_create_nonce( 'meetup-widgets' ) ); + $response = rest_do_request( $request ); + $groups = $response->get_data(); + + if ( isset( $groups['code'] ) || count( $groups ) < 1 ) { + return []; + } + + return $groups; + } +} diff --git a/widgets/single.php b/includes/widgets/single.php similarity index 100% rename from widgets/single.php rename to includes/widgets/single.php diff --git a/includes/widgets/user-list.php b/includes/widgets/user-list.php new file mode 100644 index 0000000..ef25a9f --- /dev/null +++ b/includes/widgets/user-list.php @@ -0,0 +1,113 @@ + 'widget_meetup_user_list', + 'description' => __( 'Display a list of events for a single user.', 'meetup-widgets' ), + ) + ); + } + + /** + * Normalize and sanitize the widget attribute values + */ + function get_sanitized_values( $attrs ) { + return array( + 'title' => isset( $attrs['title'] ) ? strip_tags( $attrs['title'] ) : '', + 'limit' => isset( $attrs['limit'] ) ? filter_var( $attrs['limit'], FILTER_VALIDATE_INT ) : 3, + 'show_desc' => isset( $attrs['show_desc'] ) ? filter_var( $attrs['show_desc'], FILTER_VALIDATE_BOOLEAN ) : false, + ); + } + + /** + * Display the widget content + * + * @see WP_Widget::widget + */ + function widget( $args, $instance ) { + $attrs = $this->get_sanitized_values( $instance ); + $title = apply_filters( 'widget_title', $attrs['title'] ); + + echo $args['before_widget']; + if ( $title ) { + echo $args['before_title'] . $title . $args['after_title']; + } + echo Meetup_Widgets_Blocks::render_block_user_list( array( + 'title' => '', + 'per_page' => $attrs['limit'], + 'show_description' => $attrs['show_desc'], + 'placeholder' => __( 'No upcoming events.', 'meetup-widgets' ), + ) ); + echo $args['after_widget']; + } + + /** + * Save the widget settings + * + * @see WP_Widget::update + */ + function update( $new_instance, $old_instance ) { + $new_values = $this->get_sanitized_values( $new_instance ); + return array_merge( $old_instance, $new_values ); + } + + /** + * Display the widget settings form + * + * @see WP_Widget::form + */ + function form( $instance ) { + $attrs = $this->get_sanitized_values( $instance ); + ?> +

+ + +

+

+ +

+

+ + +

+

+ +

+ + diff --git a/readme.txt b/readme.txt index 990a9b4..9d0613e 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === Meetup Widgets === Contributors: ryelle -Tags: meetup, meetups, meetup.com, widget, +Tags: meetup, meetups, meetup.com, widget, gutenberg Requires at least: 4.8 Requires PHP: 5.6 Tested up to: 4.9 -Stable tag: 2.3.0 +Stable tag: 3.0.0 Adds widgets displaying information from a meetup.com group. @@ -52,10 +52,17 @@ Previous to version 3.0, this plugin had a feature where you could RSVP to an ev = 3.0.0 = -* Removal of OAuth -* Refactor widgets into new `widgets/*` files -* Move templates into `templates/` folder -* Add PHP CodeSniffer, clean up flagged issues +* NEW: Gutenberg support: 2 new blocks for listing events +* NEW: Internal API endpoints for fetching Meetup.com data +* BREAKING: Removal of OAuth RSVP feature +* BREAKING: Removed themeable meetup-list template in favor of standardizing with gutenberg blocks +* BREAKING: Set minimum PHP version to 5.6 +* UPDATE: Refactor basically the entire plugin +* UPDATE: Refactor widgets into new `includes/widgets/*` files +* UPDATE: Move templates into `includes/templates/` folder +* DEVELOPER: Add PHP CodeSniffer, clean up flagged issues +* DEVELOPER: Add webpack, babel, eslint for building gutenberg blocks +* DEVELOPER: Add Handlebars for the gutenberg block templates = 2.2.1 = diff --git a/templates/meetup-list.php b/templates/meetup-list.php deleted file mode 100644 index 5d72680..0000000 --- a/templates/meetup-list.php +++ /dev/null @@ -1,23 +0,0 @@ -'; -foreach ( $events as $event ) { - printf( - '
  • %2$s; %3$s
  • ', - esc_url( $event->event_url ), - strip_tags( $event->name ), - date( 'M d, g:ia', intval( $event->time / 1000 + $event->utc_offset / 1000 ) ) - ); -} -echo ''; diff --git a/vs_meetup.php b/vs_meetup.php index 120575f..38c144e 100644 --- a/vs_meetup.php +++ b/vs_meetup.php @@ -2,11 +2,12 @@ /** * Plugin Name: Meetup Widgets * Description: Add widgets displaying information from Meetup.com - * Version: 2.2.1 + * Version: 3.0.0 * Author: Kelly Dwan * Author URI: http://redradar.net * Plugin URI: http://redradar.net/category/plugins/meetup-widgets/ * License: GPL2 + * Text Domain: meetup-widgets * Date: 01.06.2016 * * @package Meetup_Widgets @@ -17,35 +18,47 @@ } if ( ! defined( 'VSMEET_TEMPLATE_DIR' ) ) { - define( 'VSMEET_TEMPLATE_DIR', dirname( __FILE__ ) . '/templates/' ); + define( 'VSMEET_TEMPLATE_DIR', dirname( __FILE__ ) . '/includes/templates/' ); } -require_once( 'class-meetup-widgets-admin.php' ); -require_once( 'class-meetup-widget.php' ); +require_once( 'vendor/autoload.php' ); + +require_once( 'includes/class-meetup-widget.php' ); +require_once( 'includes/class-meetup-widgets-admin.php' ); +require_once( 'includes/class-meetup-widgets-blocks.php' ); +require_once( 'includes/class-meetup-api-v3.php' ); +require_once( 'includes/api/class-meetup-rest-events-controller.php' ); +require_once( 'includes/api/class-meetup-rest-groups-controller.php' ); +require_once( 'includes/widgets/single.php' ); +require_once( 'includes/widgets/group-list.php' ); +require_once( 'includes/widgets/user-list.php' ); /** * Initialize Meetup Widgets */ function meetup_widgets_start() { new Meetup_Widgets_Admin(); + new Meetup_Widgets_Blocks(); + $event_controller = new Meetup_REST_Events_Controller(); + $event_controller->register_routes(); + + $group_controller = new Meetup_REST_Groups_Controller(); + $group_controller->register_routes(); } add_action( 'init', 'meetup_widgets_start' ); -require_once( 'widgets/single.php' ); add_action( 'widgets_init', function() { return register_widget( 'VsMeetSingleWidget' ); } ); -require_once( 'widgets/group-list.php' ); add_action( 'widgets_init', function() { return register_widget( 'VsMeetListWidget' ); } ); -require_once( 'widgets/user-list.php' ); add_action( 'widgets_init', function() { return register_widget( 'VsMeetUserListWidget' ); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..2f6a741 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,59 @@ +/** @format */ + +const webpack = require( 'webpack' ); +const ExtractTextPlugin = require( 'extract-text-webpack-plugin' ); + +// CSS loader for styles specific to blocks in general. +const blocksCSSPlugin = new ExtractTextPlugin( { + filename: './build/editor.css', +} ); + +// Configuration for the ExtractTextPlugin. +const extractConfig = { + use: [ + { loader: 'raw-loader' }, + { + loader: 'postcss-loader', + options: { + plugins: [ require( 'autoprefixer' ) ], + }, + }, + ], +}; + +module.exports = { + entry: __dirname + '/includes/blocks/src/index.js', + output: { + filename: 'build/index.js', + path: __dirname, + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: 'babel-loader', + }, + { + test: /\.hbs$/, + exclude: /node_modules/, + use: 'handlebars-loader', + }, + { + test: /style\.css$/, + include: [ /blocks/ ], + use: blocksCSSPlugin.extract( extractConfig ), + }, + ], + }, + plugins: [ + new webpack.DefinePlugin( { + 'process.env.NODE_ENV': JSON.stringify( process.env.NODE_ENV || 'development' ), + TEMPLATE_DIRECTORY: JSON.stringify( __dirname + '/includes/templates' ), + } ), + blocksCSSPlugin, + ], + devServer: { + port: 8081, + }, +}; diff --git a/widgets/group-list.php b/widgets/group-list.php deleted file mode 100644 index de8712c..0000000 --- a/widgets/group-list.php +++ /dev/null @@ -1,98 +0,0 @@ - 'widget_meetup_group_list', - 'description' => __( 'Display a list of events.', 'meetup-widgets' ), - ) - ); - } - - /** - * Display the widget content - * - * @see WP_Widget::widget - */ - function widget( $args, $instance ) { - $title = apply_filters( 'widget_title', $instance['title'] ); - $id = $instance['id']; // meetup ID or URL name - $limit = intval( $instance['limit'] ); - - echo $args['before_widget']; - if ( $title ) { - echo $args['before_title'] . $title . $args['after_title']; - } - if ( $id ) { - $vsm = new Meetup_Widget(); - $html = $vsm->get_group_events( $id, $limit ); - echo $html; - } - echo $args['after_widget']; - } - - /** - * Save the widget settings - * - * @see WP_Widget::update - */ - function update( $new_instance, $old_instance ) { - $instance = $old_instance; - $instance['title'] = strip_tags( $new_instance['title'] ); - if ( preg_match( '/[a-zA-Z]/', $new_instance['id'] ) ) { - $instance['id'] = sanitize_title( $new_instance['id'] ); - } else { - $instance['id'] = str_replace( ' ', '', $new_instance['id'] ); - } - $instance['limit'] = intval( $new_instance['limit'] ); - - return $instance; - } - - /** - * Display the widget settings form - * - * @see WP_Widget::form - */ - function form( $instance ) { - if ( $instance ) { - $title = esc_attr( $instance['title'] ); - $id = esc_attr( $instance['id'] ); // -> it's a name if it contains any a-zA-z, otherwise ID - $limit = intval( $instance['limit'] ); - } else { - $title = ''; - $id = ''; - $limit = 5; - } - ?> -

    -

    -

    - - -

    - 'widget_meetup_user_list', - 'description' => __( 'Display a list of events for a single user.', 'meetup-widgets' ), - ) - ); - } - - /** - * Display the widget content - * - * @see WP_Widget::widget - */ - function widget( $args, $instance ) { - $title = apply_filters( 'widget_title', $instance['title'] ); - $limit = absint( $instance['limit'] ); - - echo $args['before_widget']; - if ( $title ) { - echo $args['before_title'] . $title . $args['after_title']; - } - $vsm = new Meetup_Widget(); - $html = $vsm->get_user_events( $limit ); - echo $html; - echo $args['after_widget']; - } - - /** - * Save the widget settings - * - * @see WP_Widget::update - */ - function update( $new_instance, $old_instance ) { - $instance = $old_instance; - $instance['title'] = strip_tags( $new_instance['title'] ); - $instance['limit'] = absint( $new_instance['limit'] ); - - // remove caching of old event - if ( ! empty( $old_instance['id'] ) ) { - delete_transient( 'vsmeet_user_events_' . $old_instance['id'] ); - } - - return $instance; - } - - /** - * Display the widget settings form - * - * @see WP_Widget::form - */ - function form( $instance ) { - if ( $instance ) { - $title = esc_attr( $instance['title'] ); - $limit = absint( $instance['limit'] ); - } else { - $title = ''; - $limit = 5; - } - ?> -

    -

    - - -

    -

    -