@blaze-cms/react-page-builder v0.136.1
Page Builder
Package for FE components based on the available page builder components in admin
Components
Image
Displays an image or multiple images using the file ids in the settings
Settings
- isHero: adds hero class. This will also add the
hero
to thesizeKey
used for the responsive image - modifer: adds class to the wrapper This will also add the modifier to the
sizeKey
used for the responsive image.
Responsive sizeKey
The sizeKey
is used to identifiy responsive image settings in responsive image config. It is set in a hierachy based on the options set
- default:
image
- isHero=true:
image:hero
- modifier set:
image:modifier-value
- isHero and modifier set:
image:hero:modifier-value
Button
The following button component properties support dynamic values using handlebar templates.
- url e.g.
/search?={{ currentParent.name }}
- text
- after click text
- url
Textblock
The textblock renders without a wrapper unless a modifier is set OR the BLAZE_PB_TEXTBLOCK_WRAPPER_ENABLE
env variable is set to true.
This env variable is depreciated so will be removed in the future.
Row / Column
Row and column components are used for general grid/layout and render their children components.
Settings
- backgroundImage: file Id for image that should be used as background image
- title: if set it will render a title inside the container
- tagType: set the tag that should be used to render the container e.g. dev, article
Banner
The banner component uses react-dfp to render DFP ad slots with settings supplied by the component and size settings.
While loading it uses the responsive sizes specificed in the API to set min-heights for the adslots e.g.
<style>
@media(min-width:320px){.banner-sizeId{min-height:100px;}
@media(min-width:728px){.banner-sizeId{min-height:100px;}
@media(min-width:970px){.banner-sizeId{min-height:250px;}
</style>
<div
class="ad-slot ad-slot-loading banner-sizeId"
></div>
...
Once the adSlot has loaded the styles are removed and the wrapper class updated. The classes are | Name | Description | |---|---| | ad-slot-loading | When adslot is loading | | ad-slot-loaded | Adslot is loaded with and ad | | ad-slot-empty | Adslot is loaded but no ad was loaded from dfp |
renderCounter
This prop can be used to indicate the posn
targeting that should be used. It is useful when generating additional banner components from a base component as it allows them to be easy to target individually in DFP.
getParsedSizes
This is a helper that enables backwards compatibility for the data when banner could have sizes saved as string. Newly created banners should all have field sizeId (instead sizes), which is a select to chose from defined banner sizes.
setCustomTargetings
Helper used to build key-value targetings to be set on banner based on the customTargeting field in admin. A key can be defined to have multiple values (set in array), so internally is making use of the buildTargetingValue
function.
buildParsedAdunit
This helper is used to build the adunit correctly. The adunit will contain the base, any middle part that depend on the url & children and end part. Base can be set in admin or if not the value of the DFP_BASE_ADUNIT
env variable will be used. If the specific entity where banner is placed has children, all part of the url will be used; otherwise the last part of the url will be taken out. End part can be set in admin, in case it's not the value of constant END_ADUNIT
is used (maybe in the future can be set with env variable so it can be project specific).
buildBannerSizes
Based on the received sizeId and the existing banner sizes from DB, the banner size array is built (array of arrays with width and height). In case it's old data, getBannerSizes
will be called.
buildContextualTargeting
Configured in admin using propsToDisplay, for each prop a label can be set and that will be the key; the value of the key-value pair will be the value of the chosen propToDisplay on that entity. At the end, it returns an object with key-value pairs.
buildSizeMapping
Returns array specifying which banner sizes should show on which viewport. Size mapping is used to make the banner responsive, to display different sizes on different screen sizes. If the viewport has value [1024, 768]
, it will use the sizes set for it when it goes over 1024.
Note: targetings (both contextual and custom) set for a banner can be viewed in the Network tab, in Headers -> Query String Parameters under the key prev_scp
.
Source for checking ads https://dfpgpt.appspot.com/.
Item List components
There are three components related to item lists
- itemListButton: add/remove items from list
- itemListCounter: shows a counter of items in a list
- itemListNew: Clears the stored local list
List components are link by the "listName" property and when a new list is created the id of the list is stored in local storage.
Card
Options
displayCarousel: will wrap the card block with left and right arrows and allow users to scroll through the cards
- If itemsPerRow is also set it will add a class to the wrapper to indicate the desired number of cards that should display e.g
card-carousel__content--items-per-row-4
- If itemsPerRow is also set it will add a class to the wrapper to indicate the desired number of cards that should display e.g
displayCategory: if true this will fetch the alternativePreHeader or category set on the record and display in the card. If the alternativePreHeader is set this takes priority over the category. If this option is not set these properties will not be included in the query.
- displayThumbnail: if true this will fetch the default card image url to displayed the the card
- propsToDisplay:
- htmlAttribute: allows setting of a custom html attribute on the prop to display. The value will be escaped e.g.
<span html-attribute-value="ESCAPED%20VALUE">
- htmlAttribute: allows setting of a custom html attribute on the prop to display. The value will be escaped e.g.
Code
The code block can parse a string of HTML and render it as react components. The code block will activate script blocks in the html string by wrapping them in a <span>
and then inserting the script tag into the DOM on render using appendChild
.
The script tags will then appear in the places they are supposed to other than the span wrapper.
Note Script tags won't get rendered on the server just in the browser.
Iframe
The iframe component renders an iframe based on the src property with dynamic height/width resizing. It accepts the following properties:
- src (required to render)
- modifier
- height
- width
SearchFilter
The url field is used to know if by clicking on the search button (or by interacting with some filter types that are trigerring the search immediately - checkbox & select, when no url is set) the filter will be applied on the same page (if no url set) or on another page (specified in that url).
Search filter is applied only to list page builder element. In packages/blaze-nextjs-tools/src/containers/ContentContainer.js
we are checking (recursively, using getSearchFilter
) if there is a search filter present on the page, if there is it put the filter's config inside the options object under the searchFilter key. That way it can be read and checked if exists on list component and apply the filter on its items.
Search filter works by appending the query params to the url, this way you can link to a search result page directly if you set the params correctly and the page will open with the filters already applied.
In the admin, different filter types can be set - text search, checkbox, select, range. Some fields are shown just for specific fitler types. Checkbox and select have a limit in the propsToDisplay to one, so only one propsToDisplay can be selected. Range can have multiple props, in cases where there is a prop that can be represented with different values (e.g. price: $/€/£, length: m/inch etc.). Then in addition to the range slider, a select is also displayed for the user to choose the unit. Range interval field is displayed only if range filter chosen. Operator is used only for checkbox and text-search filter. Checkbox takes advantage of the operator in case there are more values checked (but for the same prop), operator sets the connections between them (is it OR or AND). While the text-search uses the operator as well, it does in the opposite way - user can choose multiple props in admin but enter only one value in FE so the operator defines the connections between the value for one and the other chosen prop.
helpers
buildQueryParams
Helper to return all query params for the applied filters. Text search filter is the easiest, it just takes the values and sets it to search_term
- that's the query param used for the text search. For the other filter, it calls the getSearchValues
internally and finally it joins all the params with &
.
buildRawQueryStringified
Returns JSON.stringified object to be passed to the searchPublishedContent
query when creating the values for the checkbox, range and select filters. It is needed to get all the unique values of the chosen field among the published records of the entity. For this query, ELKS aggregations are used. The values from the query are then used within the checkbox/select to display the different options to check/select and to define the ranges (min/max) of the range filter.
calculateStep
Used to calculate step to be added to be max range value. It can happen that in the case of range interval being some value different than 1, the min & max range values need to be updated. E.g. min value 13, max value 37 and step 3, means min value needs to be the highest value lower than 13 that is divisible by 3 -> 12 and max needs to be the lowest value higer than 37 divisible by 39. To get the value to be added on the set max value, calculateStep is called calculateStep(37, 3) = rangeInterval - 1 = 2; max = max + 2 = 39
.
checkIfRangeUpdated
Checks the current min/max values are different from the initial, meaning the range was interacted with (it was updated).
decode-encode
Contains functions to decode and encode the passed value.
getDisplayValue
Used for range filter, in the case of multiple props. Check for existance of unit property for the chosen prop to display from the select.
getIntersectedProp
Returns any specific prop, available for the range filter and present in the query params.
getSearchValues
Builds the checkbox, select and range query params. For checkbox it checks whether the value for the prop is an array, that would mean multiple checkbox have been selected for the same prop. For the range, it gets the values from the DOM elements (only for the updated ranges, ones set in localStorage).
getSelectOptions
Returns select options to display in case of range filter with multiple props so that it calls the getDisplayValue
function to display the unit value if it exists.
getUpdatedRanges
Returns the updated ranges from the localStorage.
removePropsFromUpdatedRanges
Updates the localStorage value of updated ranges to exclude the passed props.
removeUpdatedRanges
Clears the value of updated ranges from localStorage.
setUpdatedRangesInStorage
Gets called from setUpdatedRanges
function and is where the actual saving in the localStorage happens.
setUpdatedRanges
In case there are query params for the range filter, it selects the prop and sets its value to the localStorage.
Note: if the issue Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [salary] in order to load field data by uninverting the inverted index. Note that this can use significant memory.
happens, it means that somewhere the keyword
word is not put for the text field.
Responsive Images config keys
component | sizeKey |
---|---|
card | card |
carousel | carousel |
image | image |
globalLightbox | globalLightbox |
lighbox | lighbox |
textBlock | textBlock |
headerSocial | socialMeta |
Helpers
buildInheritedFilters
If the helper getInheritedFilters
(explained below) returns values - it means that the filter values returned will need to be inherited from the parent entity where the page builder item is placed. If we got an array with category.name/category
in the card/list, we need to check the category { name }
of the parent entity (entity where the PB item is placed). If there is no category set, we return nothing. In case there is a category set, we need to add it to the filter values (the values to filter on get merged with the filter values set directly in admin - the ones that are properly set in filterByProperty & filterBy fields).
buildQueryFields
Function used to build the selection to be requested on a query, specifically the searchPublishedContent
queries used for cards and lists. We are getting the results and total; results will have a subselection that is a fragment with the necessary fields, __typename
(since we are using the fragment), id and pageBuilderComponents
in case the query is done for a list of type full.
buildRawQueryBase
Used by card directly (as cards use Elasticsearch for queries but the search filter doesnt need to be applied so only the base is needed) and the buildRawQuery
helper calls it internally to build the object for the rawQueryStringified query variable.
Based on the operator that can be chosen for the filterByProperty and filterBy fields, object differs. Also depends if there are any itemsToDisplay chosen.
buildRawQuery
This function uses the buildRawQueryBase
to build the base of the object (the values based on the filter fields in card/list). In the case where itemsToDisplay are chosen, it builds correctly that part and also applies the sort on the itemsToDisplay in case set - otherwise records are sorted how they get returned from ELKS. The rest of the logic is to get the applied search filter and build the value. From the filter we can have checkbox-select-range values (for checkbox the values are grouped with the rest in case the operator selected is AND), if some checkbox filter has OR operator values will be already in another variable and dealt separately. Also for text search, the operator is checked so the final value can consist of different combinations of AND/OR filters grouped accordingly (under the ELKS operators must/should).
buildSearchValuesCheckboxSelect
Goes over the applied search filter (reads query param) and builds the variables to be used in query that can be grouped under the MUST (values) or SHOULD (valuesOr) filter. For checkbox, range and select filters their names are used in building the query param key-value pairs and for text-search the key is always search_term
.
If the query param is a string, it can be checkbox, range or select. For the range the logic is slightly different so it's separated in an if. If the query param value is an array, that can be only from a checkbox filter, we need to get the operator (AND/OR) and finally we build the object to return.
buildSearchValuesText
Returns array with correctly built text search filter values in the format for ELKS.
buildSetFilters
Helper to build the filters that are set on the list/card directly (in the filterByProperty, filterBy fields and filterByFeatured, filterBySponsored toggles). Returns object with checkboxFilters that contain featured/sponsored keyword in case set in admin and listFilterValues that contains object for each field we want to filter on and the array of chosen values for that specific field.
getElasticsearchOperator
Based on the selected operator in admin (AND/OR) for the text-search search filter, it will return the correct Elasticsearch operator (MUST for AND, SHOULD for OR).
getEntityData
For the entity name passed, return the docType (prepend the keyword published_
) and entityType (prepend the keyword Published
and change entity name that has underscores to become camelcased with first letter in upper case). E.g. page -> docType: published_page, entityType: PublishedPage; some_other_entity -> docType: published_some_other_entity, entityType: PublishedSomeOtherEntity
getGenericRenderVariables
Function used in cards and lists to build and set all the variables needed for the query (limit, offset, sort, rawQueryStringified).
Limit and offset can be set in admin, otherwise a default value is applied. Sort is build based on the admin config; in the case there is a text search filter applied, no sort is applied. Otherwise sort is built based on the values set in sort & sortby fields. It needs to be taken into consideration that we are using Elasticsearch queries for cards/lists which treats text fields and the rest of the fields differently so in the case of text fields (fields of type string) to append .keyword
to the field name. That is explained in more details in getSortbyFieldName
.
getInheritedFilters
For the page builder items that have the filterBy and filterByProperty fields, it checks if there is a filterByProperty set but there is no specific record set to filter on. For example, if there is a filterByProperty set (category.name/category
) but in the filterBy field there is no category.name specified.
getImageId
Returns the correct id of the image that needs to be fetched from the API using getFile
query. If it supposed to be an image from relation, the imageId set on the relation is being read. In case the relation has an array of image ids, the first image id will be used. In the default case, the image id of the uploaded/chosen image in admin is used.
getQueryFilters
Returns the object with the correctly set array of filter values that need to be under the MUST elasticsearch operator (valuesAnd) and the ones for SHOULD (valuesOr). Internally it makes use of the getSearchFilterType
function to get range, select and checkbox filters and buildSearchValuesCheckboxSelect function.
getQueryProps
Helper for building query props from the filters that need to be inherited from the parent entity. Goes over the array of inheritedFilters that can have the format [categoryId/category, tagIds/tag]
. It will set the queryProps to contain categoryId and tagIds.
getSearchFilterType
Returns any filters of the type specified as the second argument passed to the function.
getSortbyFieldName
Returns the value for the sort, based on the chosen field - we check if the field is in the stringProps array (if it's a relation field category.name
we need to check it with the correct entityIdentifier so we are passing the relations array as well). If it's a string type prop, we need to append .keyword
.
In the case of a relation it can happen that the chosen field is authors.name
but the actual entity name is author and stringProps array contains the field names like this author.name, author.firstname etc so we need to update the value for sort to use the actual entityIdentifier (author.name). In admin you can also choose the type of sort (ASC/DESC) so the final value we return could be something like this author.name.keyword:ASC
.
getUnpublishedEntityName
Returns the unpublished version of the entity's name (E.g. published_page -> page; published_some_name -> some_name).
getUpdatedFilterBy
hasChildren
Function used in Button, Layout (column/row) and Modal in order to check if the specific element has children nested since some of the elements should not be rendered in case they dont have nexted elements/children (in addition to some other condition in the case of Button and Layout).
isBrowser
Helper to check if the window is undefined, to know if the code executing is in the client or server side.
isFilterEntitysId
In the case where the filterByProperty has a value like: categoryId/category, check if the itemEntity (we get it from the parent and then get the unpublished version of the name) is actually the chosen relation. In the case of categoryId/category
and itemEntity is category
the functon will return true meaning that the actual value to use in the filter is the id of the entity (itemId) in the buildInheritedFilters
function. When it's called in the getQueryProps
helper it means it will add the id to the query props and not categoryId because the query will break as it will actually requesting categoryId on itself (getCategory { categoryId }
) but in that case all we need is the actual entity's id (getCategory { id }
).
isUsingRelationImage
Helper that checks if the image component has it set to fetch the image from a relation. It needs to have an entity selected, the toggle fetchFromRelation
set to true and a value in the imageRelation field.
updateChildrensParent
When rendering card item and card item's children they should know that its parent entity is the card item's entity and not the entity where the card is placed. Meaning if a card is placed on a page, but it's rendering children, those children will have the card's entity (e.g. yacht_for_sale instead of page) and the id of that specific entity record for the parent itemEntity and itemId.
removeExtraItems
When rendering cards in order to make the queries as cacheable as possible if the card entity is the same as the current entity we increase the limit by 1. Then use this function to remove the current item if it is in the results or reduce the array to the correct size.
Hooks
helpers
buildPBComponents
Main function to build the list of components to render. The other logic found here is used for the banner repetitions that is explained in the next 3 functions below.
checkBannerInsertionSet
If the component has children (component.items), check for banner component and if it has the interval set. The check will be done just on the children at the first level. Return the banner if the conditions match.
getBannerIndex
If helper checkBannerInsertionSet
returned a banner, use this function to get the exact index of the banner component among the component's first level children.
Assumption: there is only one banner that should be interscrollar at the same level.
insertBanners
This helper goes over component's first level children and based on the banner's settings (interval & repeat) inserts the banner at specific places (on the same level).
The number(s) in the interval field = the position(s) after which the banner should be inserted.
There are four cases to deal with:
- repeat toggle ON, interval is just one number
- E.g. repeat: true; interval: '2'
before: ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM
after: ITEM ITEM BANNER ITEM ITEM BANNER ITEM ITEM BANNER ITEM ITEM BANNER
- repeat toggle ON, interval has more numbers separated by commas
- E.g. repeat: true; interval: '2,5,7'
before: ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM
after: ITEM ITEM BANNER(2) ITEM ITEM BANNER(2) ITEM BANNER(5) ITEM BANNER(2) ITEM BANNER(7) ITEM BANNER(2) ITEM ITEM BANNER(2&5) ITEM ITEM BANNER(2)
- repeat toggle OFF, interval is just one number
- E.g. repeat: false; interval: '2'
before: ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM
after: ITEM ITEM BANNER ITEM ITEM ITEM ITEM ITEM ITEM
- repeat toggle OFF, interval has more numbers separated by commas
- E.g. repeat: false; interval: '2,5,7'
before: ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM ITEM
after: ITEM ITEM BANNER ITEM ITEM ITEM BANNER ITEM ITEM BANNER ITEM ITEM ITEM ITEM ITEM
Environment variables
Env | Description | Default |
---|---|---|
BLAZE_PB_TEXTBLOCK_WRAPPER_ENABLE | Render legacy texblock component wrapper | false |
BLAZE_SCROLL_OFFSET | Offest to use when scrolling after an event. Can be set if there are sticky elements on the page to take them into account | false |
BLAZE_PB_SEARCH_FILETER_AGG_SIZE | Override default size property for search filter aggregations | 500 |
BLAZE_PB_ADD_CLICK_TO_CARDS | Add click handler to card parent | false |
9 months ago
9 months ago
10 months ago
10 months ago
10 months ago
5 months ago
5 months ago
5 months ago
5 months ago
10 months ago
10 months ago
8 months ago
8 months ago
7 months ago
7 months ago
8 months ago
9 months ago
9 months ago
9 months ago
7 months ago
5 months ago
6 months ago
9 months ago
6 months ago
8 months ago
8 months ago
10 months ago
6 months ago
6 months ago
10 months ago
5 months ago
9 months ago
5 months ago
9 months ago
9 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
8 months ago
8 months ago
8 months ago
7 months ago
8 months ago
10 months ago
10 months ago
6 months ago
6 months ago
7 months ago
8 months ago
8 months ago
8 months ago
7 months ago
7 months ago
7 months ago
7 months ago
8 months ago
5 months ago
6 months ago
10 months ago
9 months ago
10 months ago
11 months ago
11 months ago
10 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
12 months ago
12 months ago
12 months ago
11 months ago
12 months ago
12 months ago
11 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
1 year ago
1 year ago
1 year ago
2 years ago
1 year ago
2 years ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago