<commit_before>angular.module('PostsApp', [])
.controller('AddPostController', ['$scope', '$http', '$rootScope', function($scope, $http, $rootScope) {
	$scope.post = {};
	$scope.addPost = function() {
		$('#addPostModal').modal();
	};

	$scope.submitPost = function(item, event) {
		$http.post('/post', $scope.post).success(function(response) {
			console.log("got response:" + JSON.stringify(response));
			$rootScope.$emit('refresh');
		});
	};
}])
.controller('PostListController',['$scope', '$http', '$rootScope', function($scope, $http, $rootScope) {

	var refresh = function() {
		$http.get('/posts').success(function(response) {
			$scope.postList = response;
		});
	};

	refresh();

	$rootScope.$on('refresh', function(event, data){
		refresh();
	});

	$scope.deletePost = function(id) {
		$http.delete('/post/' + id).success(function(response){
			console.log('Successfully deleted' + response._id);
			refresh();
		});
	};

	$scope.updatePostModal = function(id) {
		$http.get('/post/' + id).success(function(response) {
    		$scope.update = response;
			$('#updatePostModal').modal();
  		});
	};

	$scope.updatePost = function(item, event) {
		$http.put('/post/' + $scope.update._id, $scope.update).success(function(response){
			console.log('Successfully updated' +  response._id);
			refresh();
		});
	};
}]);<commit_msg>FixBug: Stop event propogation for post delete<commit_after>angular.module('PostsApp', [])
.controller('AddPostController', ['$scope', '$http', '$rootScope', function($scope, $http, $rootScope) {
	$scope.post = {};
	$scope.addPost = function() {
		$('#addPostModal').modal();
	};

	$scope.submitPost = function(item, event) {
		$http.post('/post', $scope.post).success(function(response) {
			console.log("got response:" + JSON.stringify(response));
			$rootScope.$emit('refresh');
		});
	};
}])
.controller('PostListController',['$scope', '$http', '$rootScope', function($scope, $http, $rootScope) {

	var refresh = function() {
		$http.get('/posts').success(function(response) {
			$scope.postList = response;
		});
	};

	refresh();

	$rootScope.$on('refresh', function(event, data){
		refresh();
	});

	$scope.deletePost = function(id, $event) {
		$event.stopPropagation();
		$http.delete('/post/' + id).success(function(response){
			console.log('Successfully deleted' + response._id);
			refresh();
		});
	};

	$scope.updatePostModal = function(id) {
		$http.get('/post/' + id).success(function(response) {
    		$scope.update = response;
			$('#updatePostModal').modal();
  		});
	};

	$scope.updatePost = function(item, event) {
		$http.put('/post/' + $scope.update._id, $scope.update).success(function(response){
			console.log('Successfully updated' +  response._id);
			refresh();
		});
	};
}]);<|endoftext|>


<commit_before>'use strict';

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BabiliPlugin = require('babili-webpack-plugin');
const path = require('path');

const isProduction = process.env.NODE_ENV === 'production'

const config = {
    entry: './src/js/app.js',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {
                                minimize: isProduction,
                            },
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                plugins: [
                                    require('stylelint'),
                                    require('postcss-nesting'),
                                ],
                            },
                        },
                    ],
                }),
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            plugins: ['syntax-dynamic-import'],
                        },
                    },
                    'eslint-loader',
                ],
            }
        ],
    },
    output: {
        chunkFilename: '[hash].js',
        filename: 'app-[hash].js',
        path: path.resolve(__dirname, 'assets'),
        publicPath: '/assets/',
    },
    plugins: [
        new CleanWebpackPlugin(['assets'], {
            watch: true,
        }),
        new ExtractTextPlugin('style-[contenthash].css'),
    ],
};

let minify = false;

if (isProduction) {
    Array.prototype.push.apply(config.plugins, [
        new BabiliPlugin(),
    ]);

    minify = {
        collapseBooleanAttributes: true,
        collapseWhitespace: true,
        conservativeCollapse: true,
        removeAttributeQuotes: true,
        removeComments: true,
        removeOptionalTags: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true,
    };
}

Array.prototype.push.apply(config.plugins, [
    new HtmlWebpackPlugin({
        filename: path.resolve(__dirname, 'index.html'),
        minify,
        template: './src/index.html',
    }),
]);

module.exports = config;
<commit_msg>Set correct publicPath for Webpack modules<commit_after>'use strict';

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BabiliPlugin = require('babili-webpack-plugin');
const path = require('path');

const isProduction = process.env.NODE_ENV === 'production'

const config = {
    entry: './src/js/app.js',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {
                                minimize: isProduction,
                            },
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                plugins: [
                                    require('stylelint'),
                                    require('postcss-nesting'),
                                ],
                            },
                        },
                    ],
                }),
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            plugins: ['syntax-dynamic-import'],
                        },
                    },
                    'eslint-loader',
                ],
            }
        ],
    },
    output: {
        chunkFilename: '[hash].js',
        filename: 'app-[hash].js',
        path: path.resolve(__dirname, 'assets'),
        publicPath: 'assets/',
    },
    plugins: [
        new CleanWebpackPlugin(['assets'], {
            watch: true,
        }),
        new ExtractTextPlugin('style-[contenthash].css'),
    ],
};

let minify = false;

if (isProduction) {
    Array.prototype.push.apply(config.plugins, [
        new BabiliPlugin(),
    ]);

    minify = {
        collapseBooleanAttributes: true,
        collapseWhitespace: true,
        conservativeCollapse: true,
        removeAttributeQuotes: true,
        removeComments: true,
        removeOptionalTags: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true,
    };
}

Array.prototype.push.apply(config.plugins, [
    new HtmlWebpackPlugin({
        filename: path.resolve(__dirname, 'index.html'),
        minify,
        template: './src/index.html',
    }),
]);

module.exports = config;
<|endoftext|>


<commit_before>module.exports = {
    extends: [
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended",
        "prettier",
        "prettier/react",
        "prettier/@typescript-eslint",
        "plugin:import/recommended",
        "plugin:import/typescript",
    ],
    parser: "@typescript-eslint/parser",
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        project: "./tsconfig.json",
        ecmaVersion: 2018,
        sourceType: "module",
    },
    overrides: [
        {
            files: ["*.ts", "*.tsx"],
        },
    ],
    rules: {
        // These rules are only preliminary to ease the migration from tslint, many of them might make sense to have enabled!
        "@typescript-eslint/camelcase": "off",
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-empty-interface": "warn",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/no-for-in-array": "warn",
        "@typescript-eslint/no-inferrable-types": "off",
        "@typescript-eslint/no-namespace": "off",
        "@typescript-eslint/no-non-null-assertion": "off",
        "@typescript-eslint/no-this-alias": "off",
        "@typescript-eslint/no-unused-vars": "warn",
        "@typescript-eslint/no-use-before-define": "off",
        "@typescript-eslint/no-var-requires": "off",
        "@typescript-eslint/restrict-plus-operands": "warn",
        "no-restricted-imports": [
            "warn",
            {
                patterns: ["../*"],
            },
        ],
        "no-console": ["warn", { allow: ["warn", "error"] }],
        "no-var": "off",
        "prefer-const": ["warn", { destructuring: "all" }],
        "react/display-name": "off",
        "react/jsx-key": "warn",
        "react/jsx-no-target-blank": "off",
        "react/no-render-return-value": "off",
        "react/no-unescaped-entities": "off",
        "react/prop-types": "off",
    },
    settings: {
        react: {
            version: "detect",
        },
        "import/resolver": "webpack",
    },
}
<commit_msg>chore(eslint): enable `react/jsx-no-target-blank`<commit_after>module.exports = {
    extends: [
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended",
        "prettier",
        "prettier/react",
        "prettier/@typescript-eslint",
        "plugin:import/recommended",
        "plugin:import/typescript",
    ],
    parser: "@typescript-eslint/parser",
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        project: "./tsconfig.json",
        ecmaVersion: 2018,
        sourceType: "module",
    },
    overrides: [
        {
            files: ["*.ts", "*.tsx"],
        },
    ],
    rules: {
        // These rules are only preliminary to ease the migration from tslint, many of them might make sense to have enabled!
        "@typescript-eslint/camelcase": "off",
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-empty-interface": "warn",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/no-for-in-array": "warn",
        "@typescript-eslint/no-inferrable-types": "off",
        "@typescript-eslint/no-namespace": "off",
        "@typescript-eslint/no-non-null-assertion": "off",
        "@typescript-eslint/no-this-alias": "off",
        "@typescript-eslint/no-unused-vars": "warn",
        "@typescript-eslint/no-use-before-define": "off",
        "@typescript-eslint/no-var-requires": "off",
        "@typescript-eslint/restrict-plus-operands": "warn",
        "no-restricted-imports": [
            "warn",
            {
                patterns: ["../*"],
            },
        ],
        "no-console": ["warn", { allow: ["warn", "error"] }],
        "no-var": "off",
        "prefer-const": ["warn", { destructuring: "all" }],
        "react/display-name": "off",
        "react/jsx-key": "warn",
        "react/jsx-no-target-blank": ["warn", { allowReferrer: true }],
        "react/no-render-return-value": "off",
        "react/no-unescaped-entities": "off",
        "react/prop-types": "off",
    },
    settings: {
        react: {
            version: "detect",
        },
        "import/resolver": "webpack",
    },
}
<|endoftext|>


<commit_before>GetOption( [ 'button-sub' ], function( items )
{
	if( items[ 'button-sub' ] === true )
	{
		return;
	}

	var container = document.querySelector( '.share' );

	if( container )
	{
		var link = document.createElement( 'a' );
		link.className = 'action_btn';
		link.target = '_blank';
		link.href = GetHomepage() + 'sub/' + GetCurrentAppID() + '/';
		link.appendChild( document.createTextNode( 'View on Steam Database' ) );

		var element = document.createElement( 'div' );
		element.className = 'demo_area_button';
		element.appendChild( link );

		container.insertBefore( element, container.firstChild );
	}
} );
<commit_msg>Place button on package pages in a better place<commit_after>GetOption( [ 'button-sub' ], function( items )
{
	if( items[ 'button-sub' ] === true )
	{
		return;
	}

	var container = document.querySelector( '.game_meta_data' );

	if( container )
	{
		var element = document.createElement( 'span' );
		element.appendChild( document.createTextNode( 'View on Steam Database' ) );

		var link = document.createElement( 'a' );
		link.className = 'action_btn';
		link.target = '_blank';
		link.href = GetHomepage() + 'sub/' + GetCurrentAppID() + '/';
		link.appendChild( element );

		element = document.createElement( 'div' );
		element.className = 'block';
		element.appendChild( link );

		container.insertBefore( element, container.firstChild );
	}
} );
<|endoftext|>


<commit_before>'use strict';
$(document).ready(function(){

  if(localStorage.getItem('login') != 'true'){
    window.location.href = 'login.html';
  }
  //Event listeners for form input
  var firstLoc, spotNameValue, spotStreetValue, spotCrossValue, dayValue, cuisineValue, ratingValue, waitTimeValue, linkValue, zoomLevel, latitudeVal, longitudeVal;
  var storageArray = new Array();

  if(localStorage['storageArray'] == undefined){
   localStorage['storageArray'] = JSON.stringify(storageArray);
 }

  if(localStorage.getItem('login') === 'true'){
  var storedArray = JSON.parse(localStorage['storageArray']);
  for(var i = 0; i < storedArray.length; i++) {
    $('#unorderedList').append(storedArray[i]);
  }
 }

// Take data from form
$('#sub').click(function() {
  spotNameValue = $('#spotname').val();
  spotStreetValue = $('#spotstreet').val();
  spotCrossValue = $('#spotcross').val();
  dayValue = $('#day').val();
  cuisineValue = $('#cuisine').val();
  ratingValue = $('#rating').val();
  waitTimeValue = $('#wait-time').val();
  ratingValue = parseFloat(ratingValue).toFixed(2);
  waitTimeValue = parseInt(waitTimeValue);

//-----------------------------linkValue to be generated from function that get a google maps location from street names ------------------------------------------------------
      var geocoder;
      var firstLoc;
      var streetAddress = spotStreetValue + ", Seattle, WA, 98109, USA";
      geocoder = new google.maps.Geocoder();
      geocoder.geocode( { 'address': streetAddress },
          function(results, status) {
              firstLoc = results[0].geometry.location;
              firstLoc = JSON.stringify(firstLoc);
              firstLoc = firstLoc.replace('(', '')
              firstLoc = firstLoc.replace(')', '');
              firstLoc = firstLoc.replace('{', '');
              firstLoc = firstLoc.replace('}', '');
              firstLoc = firstLoc.replace('G', '');
              firstLoc = firstLoc.replace('K', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace(':', '');
              firstLoc = firstLoc.replace(':', '');

//-----------------------------------------------------------------------------------------------------------------------------------------------
console.log(firstLoc);
linkValue = 'See on map!';

  inputForm.waitTime();
  inputForm.getRating();

     //appends user submissons to unordered list of added locations
   var liString = '
' + spotNameValue + ' - ' + spotStreetValue + ' - ' + spotCrossValue + ' - ' + dayValue + ' - ' + cuisineValue + ' - ' + waitTimeValue + ' - ' + ratingValue + ' - ' + linkValue + '
';
   JSON.stringify(liString);
   storageArray.push(liString);
   localStorage['storageArray'] = JSON.stringify(storageArray);
   var storedArray = JSON.parse(localStorage['storageArray']);

  for(var i = 0; i < storageArray.length; i++) {
    $('#unorderedList').append(storedArray[i]);
  }

  //console.log('Button was clicked');
  //console.log(spotNameValue);
  //console.log(spotStreetValue);
  //console.log(spotCrossValue);
  //console.log(cuisineValue);
  //console.log(waitTimeValue);
  //console.log(ratingValue);
  });
});

// Constructor
var InputForm = function() {
  this.ratingArray = []; //starts at 0
  this.waitArray = []; //starts at 0
};

var inputForm = new InputForm();

// Function for averaging user input wait time
InputForm.prototype.waitTime = function() {
  this.waitArray.push(waitTimeValue); //push user input to array onclick in form
  var waitSum = 0;
  for(var i = 0; i < this.waitArray.length; i++) {
    waitSum += this.waitArray[i];
};
  var waitAvg = waitSum / this.waitArray.length;
  //console.log('The sum of all the elements is: ' + waitSum + ' The average is: ' + waitAvg);
};

// Function for averaging user input of score 1-5
InputForm.prototype.getRating = function() {
  this.ratingArray.push(ratingValue); //push use input to array onclick in form
  var ratingSum = 0;
  for(var i = 0; i < this.waitArray.length; i++) {
    ratingSum += this.ratingArray[i];
  }

  var avgRating = ratingSum / this.ratingArray.length;
  //console.log('The sum of all the elements is: ' + ratingSum + ' The average is: ' + avgRating);
  };
});<commit_msg>fixed rating boundaries<commit_after>'use strict';
$(document).ready(function(){

  if(localStorage.getItem('login') != 'true'){
    window.location.href = 'login.html';
  }
  //Event listeners for form input
  var firstLoc, spotNameValue, spotStreetValue, spotCrossValue, dayValue, cuisineValue, ratingValue, waitTimeValue, linkValue, zoomLevel, latitudeVal, longitudeVal;
  var storageArray = new Array();

  if(localStorage['storageArray'] == undefined){
   localStorage['storageArray'] = JSON.stringify(storageArray);
 }

  if(localStorage.getItem('login') === 'true'){
  var storedArray = JSON.parse(localStorage['storageArray']);
  for(var i = 0; i < storedArray.length; i++) {
    $('#unorderedList').append(storedArray[i]);
  }
 }

// Take data from form
$('#sub').click(function() {
  spotNameValue = $('#spotname').val();
  spotStreetValue = $('#spotstreet').val();
  spotCrossValue = $('#spotcross').val();
  dayValue = $('#day').val();
  cuisineValue = $('#cuisine').val();
  ratingValue = $('#rating').val();
  waitTimeValue = $('#wait-time').val();
  ratingValue = parseFloat(ratingValue).toFixed(2);
  waitTimeValue = parseInt(waitTimeValue);

//-----------------------------linkValue to be generated from function that get a google maps location from street names ------------------------------------------------------
      var geocoder;
      var firstLoc;
      var streetAddress = spotStreetValue + ", Seattle, WA, 98109, USA";
      geocoder = new google.maps.Geocoder();
      geocoder.geocode( { 'address': streetAddress },
          function(results, status) {
              firstLoc = results[0].geometry.location;
              firstLoc = JSON.stringify(firstLoc);
              firstLoc = firstLoc.replace('(', '')
              firstLoc = firstLoc.replace(')', '');
              firstLoc = firstLoc.replace('{', '');
              firstLoc = firstLoc.replace('}', '');
              firstLoc = firstLoc.replace('G', '');
              firstLoc = firstLoc.replace('K', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace('"', '');
              firstLoc = firstLoc.replace(':', '');
              firstLoc = firstLoc.replace(':', '');

//-----------------------------------------------------------------------------------------------------------------------------------------------
console.log(firstLoc);
linkValue = 'See on map!';

  inputForm.waitTime();
  inputForm.getRating();

     //appends user submissons to unordered list of added locations
   var liString = '
' + spotNameValue + ' - ' + spotStreetValue + ' - ' + spotCrossValue + ' - ' + dayValue + ' - ' + cuisineValue + ' - ' + waitTimeValue + ' - ' + ratingValue + ' - ' + linkValue + '
';
   JSON.stringify(liString);
   storageArray.push(liString);
   localStorage['storageArray'] = JSON.stringify(storageArray);
   var storedArray = JSON.parse(localStorage['storageArray']);

  for(var i = 0; i < storageArray.length; i++) {
    $('#unorderedList').append(storedArray[i]);
  }

  //console.log('Button was clicked');
  //console.log(spotNameValue);
  //console.log(spotStreetValue);
  //console.log(spotCrossValue);
  //console.log(cuisineValue);
  //console.log(waitTimeValue);
  //console.log(ratingValue);
  });
});

// Constructor
var InputForm = function() {
  this.ratingArray = []; //starts at 0
  this.waitArray = []; //starts at 0
};

var inputForm = new InputForm();

// Function for averaging user input wait time
InputForm.prototype.waitTime = function() {
  this.waitArray.push(waitTimeValue); //push user input to array onclick in form
  var waitSum = 0;
  for(var i = 0; i < this.waitArray.length; i++) {
    waitSum += this.waitArray[i];
};
  var waitAvg = waitSum / this.waitArray.length;
  //console.log('The sum of all the elements is: ' + waitSum + ' The average is: ' + waitAvg);
};

// Function for averaging user input of score 1-5
InputForm.prototype.getRating = function() {
  this.ratingArray.push(ratingValue); //push use input to array onclick in form
  var ratingSum = 0;
  for(var i = 0; i < this.waitArray.length; i++) {
    ratingSum += this.ratingArray[i];
  }
   if (ratingValue > 5) {
    alert('Choose a number between 1 and 5!');
  }

  var avgRating = ratingSum / this.ratingArray.length;
  //console.log('The sum of all the elements is: ' + ratingSum + ' The average is: ' + avgRating);
  };
});
<|endoftext|>


<commit_before>const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  devtool: 'eval',
  target: 'web',
  entry: [
    'webpack-dev-server/client?http://0.0.0.0:8080',
    'webpack/hot/only-dev-server',
    './src/index'
  ],
  output: {
    path: 'dist',
    filename: 'bundle.js',
    publicPath: '/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin()
  ],
  module: {
    loaders: [{
      test: /\.js$/,
      loaders: ['react-hot-loader', 'babel'],
      include: 'src'
    }, {
      test: /\.scss$/,
      loaders: 'sass',
      loader: 'style!css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox'
    }, {
      test: /\.css$/,
      loaders: 'style!css'
    }]
  }
};
<commit_msg>Configure module resolution<commit_after>const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  devtool: 'eval',
  target: 'web',
  entry: [
    'webpack-dev-server/client?http://0.0.0.0:8080',
    'webpack/hot/only-dev-server',
    './src/index'
  ],
  output: {
    path: 'dist',
    filename: 'bundle.js',
    publicPath: '/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin()
  ],
  resolve: {
    extensions: ['', '.js', '.scss', '.css'],
    packageMains: ['browser', 'web', 'browserify', 'main', 'style'],
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loaders: ['react-hot-loader', 'babel'],
      include: 'src'
    }, {
      test: /\.scss$/,
      loaders: 'sass',
      loader: 'style!css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox'
    }, {
      test: /\.css$/,
      loaders: 'style!css'
    }]
  }
};
<|endoftext|>


<commit_before>/*
 * mod.js, moderator page js stuff
 */

// TODO: implement mod panel all the way

document.onload = function(ev) {
  // populate the mod page with stuff
}

function get_longhash(str) {
  var idx = str.indexOf("#") + 1;
  if ( idx > 0 ) {
    str = str.substr(idx);
  }
  console.log(str);
  return str;
}

// handle ban command
function nntpchan_ban() {
  nntpchan_mod({
    parser: get_longhash,
    name: "ban",
    handle: function(j) {
      if (j.banned) {
        return document.createTextNode(j.banned);
      }
    }
  });
}

function nntpchan_unban() {
  nntpchan_mod({
    name: "unban",
    handle: function(j) {
      if (j.result) {
        return document.createTextNode(j.result);
      }
    }
  })
}

function get_board_target() {
  var e = document.getElementById("nntpchan_board_target");
  return e.value;
}

function nntpchan_admin_board(method) {
  nntpchan_admin(method, {
    newsgroup: get_board_target()
  })
}

function nntpchan_admin(method, param) {
  nntpchan_mod({
    name:"admin",
    parser: function(target) {
      return method;
    },
    handle: function(j) {
      if (j.result) {
        return document.createTextNode(j.result);
      } else {
        return "nothing happened?";
      }
    },
    method: ( param && "POST" ) || "GET"
  })
}

// handle delete command
function nntpchan_delete() {
  nntpchan_mod({
    parser: get_longhash,
    name: "del",
    handle: function(j) {
      var elem = document.createElement("div");
      if (j.deleted) {
        for ( var idx = 0 ; idx < j.deleted.length ; idx ++ ) {
          var msg = "deleted: " + j.deleted[idx];
          var e = document.createTextNode(msg);
          var el = document.createElement("div");
          el.appendChild(e);
          elem.appendChild(el);
        }
      }
      if (j.notdeleted) {
        for ( var idx = 0 ; idx < j.notdeleted.length ; idx ++ ) {
          var msg = "not deleted: " + j.notdeleted[idx];
          var e = document.createTextNode(msg);
          var el = document.createElement("div");
          el.appendChild(e);
          elem.appendChild(el);
        }
      }
      return elem;
    }
  });
}

function nntpchan_mod(mod_action) {

  // get the element
  var input = document.getElementById("nntpchan_mod_target");
  var target = input.value;
  if (mod_action.parser) {
    target = mod_action.parser(target);
  }

  var elem = document.getElementById("nntpchan_mod_result");
  // clear old results
  while( elem.firstChild ) {
    elem.removeChild(elem.firstChild);
  }

  // fire off ajax
  var ajax = new XMLHttpRequest();
  ajax.onreadystatechange = function() {
    if (ajax.readyState == XMLHttpRequest.DONE) {
      var status = ajax.status;
      // we gud?
      if (status == 200) {
        // yah
        var txt = ajax.responseText;
        var j = JSON.parse(txt);
        if (j.error) {
          var e = document.createTextNode(j.error);
          elem.appendChild(e);
        } else {
          if (mod_action.handle) {
            var result = mod_action.handle(j);
            if (result) {
              elem.appendChild(result);
            } else {
              // fail
              alert("mod action failed, handler returned nothing");
            }
          } else {
            // fail
            alert("mod action has no handler");
          }
        }
      } else if (status) {
        // nah
        // http error
        elem.innerHTML = "error: HTTP "+status;
      }
      // clear input
      input.value = "";
    }
  }
  if (mod_action.name) {
    var url = mod_action.name + "/" + target;
    ajax.open(mod_action.method || "GET", url);
    ajax.send();
  } else {
    alert("mod action has no name");
  }
}
<commit_msg>ammend mod panel js<commit_after>/*
 * mod.js, moderator page js stuff
 */

// TODO: implement mod panel all the way

document.onload = function(ev) {
  // populate the mod page with stuff
}

function get_longhash(str) {
  var idx = str.indexOf("#") + 1;
  if ( idx > 0 ) {
    str = str.substr(idx);
  }
  console.log(str);
  return str;
}

// handle ban command
function nntpchan_ban() {
  nntpchan_mod({
    parser: get_longhash,
    name: "ban",
    handle: function(j) {
      if (j.banned) {
        return document.createTextNode(j.banned);
      }
    }
  });
}

function nntpchan_unban() {
  nntpchan_mod({
    name: "unban",
    handle: function(j) {
      if (j.result) {
        return document.createTextNode(j.result);
      }
    }
  })
}

function get_board_target() {
  var e = document.getElementById("nntpchan_board_target");
  return e.value;
}

function nntpchan_admin_board(method) {
  nntpchan_admin(method, {
    newsgroup: get_board_target()
  })
}

function nntpchan_admin(method, param) {
  nntpchan_mod({
    name:"admin",
    parser: function(target) {
      return method;
    },
    handle: function(j) {
      if (j.result) {
        return document.createTextNode(j.result);
      } else {
        return "nothing happened?";
      }
    },
    method: ( param && "POST" ) || "GET",
    data: param
  })
}

// handle delete command
function nntpchan_delete() {
  nntpchan_mod({
    parser: get_longhash,
    name: "del",
    handle: function(j) {
      var elem = document.createElement("div");
      if (j.deleted) {
        for ( var idx = 0 ; idx < j.deleted.length ; idx ++ ) {
          var msg = "deleted: " + j.deleted[idx];
          var e = document.createTextNode(msg);
          var el = document.createElement("div");
          el.appendChild(e);
          elem.appendChild(el);
        }
      }
      if (j.notdeleted) {
        for ( var idx = 0 ; idx < j.notdeleted.length ; idx ++ ) {
          var msg = "not deleted: " + j.notdeleted[idx];
          var e = document.createTextNode(msg);
          var el = document.createElement("div");
          el.appendChild(e);
          elem.appendChild(el);
        }
      }
      return elem;
    }
  });
}

function nntpchan_mod(mod_action) {

  // get the element
  var input = document.getElementById("nntpchan_mod_target");
  var target = input.value;
  if (mod_action.parser) {
    target = mod_action.parser(target);
  }

  var elem = document.getElementById("nntpchan_mod_result");
  // clear old results
  while( elem.firstChild ) {
    elem.removeChild(elem.firstChild);
  }

  // fire off ajax
  var ajax = new XMLHttpRequest();
  ajax.onreadystatechange = function() {
    if (ajax.readyState == XMLHttpRequest.DONE) {
      var status = ajax.status;
      // we gud?
      if (status == 200) {
        // yah
        var txt = ajax.responseText;
        var j = JSON.parse(txt);
        if (j.error) {
          var e = document.createTextNode(j.error);
          elem.appendChild(e);
        } else {
          if (mod_action.handle) {
            var result = mod_action.handle(j);
            if (result) {
              elem.appendChild(result);
            } else {
              // fail
              alert("mod action failed, handler returned nothing");
            }
          } else {
            // fail
            alert("mod action has no handler");
          }
        }
      } else if (status) {
        // nah
        // http error
        elem.innerHTML = "error: HTTP "+status;
      }
      // clear input
      input.value = "";
    }
  }
  if (mod_action.name) {
    var url = mod_action.name + "/" + target;
    ajax.open(mod_action.method || "GET", url);
    var data = mod_action.data;
    if (data) {
      ajax.send(data);
    } else {
      ajax.send();
    }
  } else {
    alert("mod action has no name");
  }
}
<|endoftext|>


<commit_before>/** @type {import('eslint').Linter.Config} */
module.exports = {
  env: {
    jest: true,
    node: true,
  },
  extends: [
    'airbnb-base',
    'plugin:security/recommended',
    'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  ],
  parser: '@typescript-eslint/parser', // Specifies the ESLint parser
  parserOptions: {
    ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  plugins: [
    'import',
    'security',
    'jest',
  ],
  rules: {
    'jest/no-disabled-tests': [2],
    'jest/no-focused-tests': [2],
    'jest/no-identical-title': [2],
    'jest/prefer-to-have-length': [2],
    'jest/valid-expect': [2],

    '@typescript-eslint/no-empty-interface': [2],
    '@typescript-eslint/no-explicit-any': [2],
    '@typescript-eslint/no-var-requires': [2],
    'import/prefer-default-export': [0],
    'import/extensions': ['error', 'ignorePackages', {
      js: 'never',
      ts: 'never',
    }],
    // TODO: Enable these lint rules:
    // They were disabled to facilitate the transition to TypeScript
    '@typescript-eslint/explicit-function-return-type': [0],
    // These rules disabled during the dependency upgrade on 2020-07-06
    // Check these and re-enable
    '@typescript-eslint/class-name-casing': [0],
    '@typescript-eslint/ban-ts-ignore': [0],
  },
  settings: {
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts', '.tsx'],
    },
    'import/resolver': {
      node: {
        extensions: ['.js', '.ts'],
      },
    },
  },
};
<commit_msg>Enable more lint rules<commit_after>/** @type {import('eslint').Linter.Config} */
module.exports = {
  env: {
    jest: true,
    node: true,
  },
  extends: [
    'airbnb-base',
    'plugin:security/recommended',
    'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  ],
  parser: '@typescript-eslint/parser', // Specifies the ESLint parser
  parserOptions: {
    ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  plugins: [
    'import',
    'security',
    'jest',
  ],
  rules: {
    'jest/no-disabled-tests': [2],
    'jest/no-focused-tests': [2],
    'jest/no-identical-title': [2],
    'jest/prefer-to-have-length': [2],
    'jest/valid-expect': [2],

    '@typescript-eslint/no-empty-interface': [2],
    '@typescript-eslint/no-explicit-any': [2],
    '@typescript-eslint/no-var-requires': [2],
    'import/prefer-default-export': [0],
    'import/extensions': ['error', 'ignorePackages', {
      js: 'never',
      ts: 'never',
    }],
  },
  settings: {
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts', '.tsx'],
    },
    'import/resolver': {
      node: {
        extensions: ['.js', '.ts'],
      },
    },
  },
};
<|endoftext|>


<commit_before>//////
// Uses a d3-based physics simulation to calculate a reasonable layout
// of avatars within a given area. Also calculates (and returns) an
// avatar size.

function positionAvatars(width, height, parent, opinions) {
  width = width || 400
  height = height || 70

  var opinions = opinions
                   .slice()
                   .sort(function (a,b) {return a.stance-b.stance}),
      n = opinions.length, // Number of opinions
      r,  // Radius of each node
      x_force_mult = 2,
      y_force_mult = height <= 100 ? 1 : 4,
      ratio_filled = .3,
      nodes, force

  //////
  // Calculate node radius based on size of area and number of nodes
  // (2*r)^2 * length == width * height * ratio_filled
  // (width * height / length) * 2 = (2*r)^2
  // sqrt(width * height / length * 2) = r
  r = Math.sqrt(width * height / opinions.length * ratio_filled)/2
  r = Math.min(r, width/2, height/2)

  // Travis: what's the purpose of this?
  if (opinions.length > 10) {
    // Now round r up until it fits perfectly within height
    var times_fit = height / (2*r)
    r = (height / (Math.floor(times_fit))) / 2 - .001
  }

  // Initialize positions of each node
  nodes = d3.range(opinions.length).map(function(i) {
    return {
      index: i,
      radius: r,
      x: r + (width-r-r) * (i / n),
      y: r + Math.random() * (height - r-r)// r + (i * 400 / n) % (height-r-r)
    }
  })

  // see https://github.com/mbostock/d3/wiki/Force-Layout for docs
  force = d3.layout.force()
    .nodes(nodes)
    .on("tick", tick)
    .on('end', function () {console.log('simulation complete')})
    .gravity(0)
    .charge(0)
    .chargeDistance(0)
    .start()

  for (var i=0; i<opinions.length; i++)
    opinions[i].icon.style.width = opinions[i].icon.style.height = r*2 + 'px'

  // translates the opinion stance to a real x position in the bounding box
  function x_target(i) {
    return (opinions[i].stance + 1)/2 * width
  }

  // One iteration of the simulation
  function tick(e) {

    //////
    // Repel colliding nodes
    // A quadtree helps efficiently detect collisions
    var q = d3.geom.quadtree(nodes),
        i = 0
    while (++i < n)
      q.visit(collide(nodes[i]))

    //////
    // Apply standard forces
    nodes.forEach(function(o, i) {

      // Move for NaNs
      // Travis: How can a NaN occur?
      if (isNaN(o.y) || isNaN(o.x)) {
        console.error('Nan0 at', o.x, o.y)
        o.y = height/2
        o.x = x_target(o.index)//width/2
      }

      // Push node toward its desired x-location (e.g. stance)
      o.x += e.alpha * (x_force_mult * width  * .001) * (x_target(o.index) - o.x)

      // Push node downwards
      o.y += e.alpha * y_force_mult

      // Ensure node is still within the bounding box
      o.x = Math.max(r, Math.min(width  - r, o.x))
      o.y = Math.max(r, Math.min(height - r, o.y))

      // Re-position node
      opinions[i].icon.style.left = o.x - r + 'px'
      opinions[i].icon.style.top  = o.y - r + 'px'
    })
  }

  function collide(node) {
    // Travis: I understand what the 16 *does* but not the significance
    //         of the particular value. Does 16 make sense for all
    //         avatar sizes and sizes of the bounding box?
    var neighborhood_radius = node.radius + 16,
        nx1 = node.x - neighborhood_radius,
        nx2 = node.x + neighborhood_radius,
        ny1 = node.y - neighborhood_radius,
        ny2 = node.y + neighborhood_radius

    return function(quad, x1, y1, x2, y2) {

      // Repel two nodes if they overlap
      if (quad.leaf && (quad.point !== node)) {
        var dx = node.x - quad.point.x,
            dy = node.y - quad.point.y,
            dist = Math.sqrt(dx * dx + dy * dy),
            combined_r = node.radius + quad.point.radius

        if (dist < combined_r) {
          // repel both points equally in opposite directions
          separate_by = ( dist - combined_r ) / dist
          offset_x = dx * separate_by * .5
          offset_y = dy * separate_by * .5

          node.x -= offset_x
          node.y -= offset_y
          quad.point.x += offset_x
          quad.point.y += offset_y
        }
      }

      // visit subregions if we could possibly have a collision there
      return x1 > nx2
          || x2 < nx1
          || y1 > ny2
          || y2 < ny1
    }
  }

  // Returns calculated radius
  return r
}<commit_msg>.<commit_after>//////
// Uses a d3-based physics simulation to calculate a reasonable layout
// of avatars within a given area. Also calculates (and returns) an
// avatar size.

function positionAvatars(width, height, parent, opinions) {
  width = width || 400
  height = height || 70

  var opinions = opinions
                   .slice()
                   .sort(function (a,b) {return a.stance-b.stance}),
      n = opinions.length, // Number of opinions
      r,  // Radius of each node
      x_force_mult = 2,
      y_force_mult = height <= 100 ? 1 : 4,
      ratio_filled = .3,
      nodes, force

  //////
  // Calculate node radius based on size of area and number of nodes
  // (2*r)^2 * length == width * height * ratio_filled
  // (width * height / length) * 2 = (2*r)^2
  // sqrt(width * height / length * 2) = r
  r = Math.sqrt(width * height / opinions.length * ratio_filled)/2
  r = Math.min(r, width/2, height/2)

  // Travis: what's the purpose of this?
  if (opinions.length > 10) {
    // Now round r up until it fits perfectly within height
    var times_fit = height / (2*r)
    r = (height / (Math.floor(times_fit))) / 2 - .001
  }

  // Initialize positions of each node
  nodes = d3.range(opinions.length).map(function(i) {
    return {
      index: i,
      radius: r,
      x: r + (width-r-r) * (i / n),
      y: r + Math.random() * (height - r-r)// r + (i * 400 / n) % (height-r-r)
    }
  })

  // see https://github.com/mbostock/d3/wiki/Force-Layout for docs
  force = d3.layout.force()
    .nodes(nodes)
    .on("tick", tick)
    .on('end', function () {console.log('simulation complete')})
    .gravity(0)
    .charge(0)
    .chargeDistance(0)
    .start()

  for (var i=0; i<opinions.length; i++)
    opinions[i].icon.style.width = opinions[i].icon.style.height = r*2 + 'px'

  // translates the opinion stance to a real x position in the bounding box
  function x_target(i) {
    return (opinions[i].stance + 1)/2 * width
  }

  // One iteration of the simulation
  function tick(e) {

    //////
    // Repel colliding nodes
    // A quadtree helps efficiently detect collisions
    var q = d3.geom.quadtree(nodes),
        i = 0
    while (++i < n)
      q.visit(collide(nodes[i]))

    //////
    // Apply standard forces
    nodes.forEach(function(o, i) {

      // Move for NaNs
      // Travis: How can a NaN occur?
      if (isNaN(o.y) || isNaN(o.x)) {
        console.error('Nan0 at', o.x, o.y)
        o.y = height/2
        o.x = x_target(o.index)//width/2
      }

      // Push node toward its desired x-location (e.g. stance)
      o.x += e.alpha * (x_force_mult * width  * .001) * (x_target(o.index) - o.x)

      // Push node downwards
      o.y += e.alpha * y_force_mult

      // Ensure node is still within the bounding box
      o.x = Math.max(r, Math.min(width  - r, o.x))
      o.y = Math.max(r, Math.min(height - r, o.y))

      // Re-position node
      opinions[i].icon.style.left = o.x - r + 'px'
      opinions[i].icon.style.top  = o.y - r + 'px'
    })
  }

  function collide(node) {

    return function(quad, x1, y1, x2, y2) {

      // Repel two nodes if they overlap
      if (quad.leaf && (quad.point !== node)) {
        var dx = node.x - quad.point.x,
            dy = node.y - quad.point.y,
            dist = Math.sqrt(dx * dx + dy * dy),
            combined_r = node.radius + quad.point.radius

        if (dist < combined_r) {
          // repel both points equally in opposite directions
          var separate_by = ( dist - combined_r ) / dist,
              offset_x = dx * separate_by * .5,
              offset_y = dy * separate_by * .5

          node.x -= offset_x
          node.y -= offset_y
          quad.point.x += offset_x
          quad.point.y += offset_y
        }
      }

      // Visit subregions if we could possibly have a collision there
      // Travis: I understand what the 16 *does* but not the significance
      //         of the particular value. Does 16 make sense for all
      //         avatar sizes and sizes of the bounding box?
      var neighborhood_radius = node.radius + 16,
          nx1 = node.x - neighborhood_radius,
          nx2 = node.x + neighborhood_radius,
          ny1 = node.y - neighborhood_radius,
          ny2 = node.y + neighborhood_radius

      return x1 > nx2
          || x2 < nx1
          || y1 > ny2
          || y2 < ny1
    }
  }

  // Returns calculated radius
  return r
}<|endoftext|>


<commit_before>module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {
      legacyDecorators: true,
    },
  },
  plugins: ['ember', 'prettier'],
  extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:prettier/recommended'],
  env: {
    browser: true,
  },
  rules: {
    'prettier/prettier': 'error',

    'ember/no-empty-attrs': 'off',
    'ember/no-get': 'off',
    'ember/no-mixins': 'off',
    'ember/require-computed-macros': 'off',
    'ember/require-computed-property-dependencies': 'off',
  },
  overrides: [
    // node files
    {
      files: [
        '.eslintrc.js',
        '.template-lintrc.js',
        'ember-cli-build.js',
        'testem.js',
        'blueprints/*/index.js',
        'config/**/*.js',
        'lib/*/index.js',
        'server/**/*.js',
      ],
      parserOptions: {
        sourceType: 'script',
        ecmaVersion: 2018,
      },
      env: {
        browser: false,
        node: true,
      },
    },
  ],
};
<commit_msg>ESLint: Enable `ember/require-computed-macros` rule<commit_after>module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {
      legacyDecorators: true,
    },
  },
  plugins: ['ember', 'prettier'],
  extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:prettier/recommended'],
  env: {
    browser: true,
  },
  rules: {
    'prettier/prettier': 'error',

    'ember/no-empty-attrs': 'off',
    'ember/no-get': 'off',
    'ember/no-mixins': 'off',
    'ember/require-computed-property-dependencies': 'off',
  },
  overrides: [
    // node files
    {
      files: [
        '.eslintrc.js',
        '.template-lintrc.js',
        'ember-cli-build.js',
        'testem.js',
        'blueprints/*/index.js',
        'config/**/*.js',
        'lib/*/index.js',
        'server/**/*.js',
      ],
      parserOptions: {
        sourceType: 'script',
        ecmaVersion: 2018,
      },
      env: {
        browser: false,
        node: true,
      },
    },
  ],
};
<|endoftext|>


<commit_before>var GitHubApi = require('github');
var Issue = require('../models/Issue');
var User = require('../models/User');
var Vote = require('../models/Vote');
var async = require('async');
var votesController = require('./votes');
var addVotesToIssues = votesController.addVotesFor('issue');
var github = new GitHubApi({
  version: "3.0.0"
});

/**
 * GET /issues
 * View all open issues in the project
 */
exports.index = function (req, res, next) {
  github.issues.repoIssues({
    user: 'larvalabs',
    repo: 'pullup',
    state: 'open'
  }, function (err, issues) {
    if(err) return next(err);

    getIssueIds(issues, function (err, issues) {

      if(err) return next(err);

      sortIssues(issues, req.user, function (err, issues) {

        if(err) return next(err);

        res.render('issues/index', {
          title: 'Open Issues',
          issues: issues
        });

      });

    });
  });
};

// get the internal issue `_id`, and create new docs for those that don't yet exist
function getIssueIds(issues, callback) {
  if(!issues || !issues.length) return callback(null, issues);

  Issue
  .find({
    number: {
      $in: issues.map(function (issue) {
        return issue.number;
      })
    }
  })
  .exec(function (err, docs) {
    if(err) return callback(err);

    var docsByNumber = {};

    docs.forEach(function (doc) {
      docsByNumber[doc.number] = doc;
    });

    async.map(issues, function (issue, cb) {
      var doc = docsByNumber[issue.number];

      if(doc) {
        // this issue has been created in our db already
        issue._id = doc._id;
        cb(null, issue);

        // UPDATE VOTE AFTER SENDING BACK THE CALLBACK
        // this doesn't need happen before a user gets the information back,
        // it can happen in the background
        castFirstIssueVote(doc, issue.user.login);

      } else {
        // we need to create a new issue
        doc = new Issue({
          number: issue.number
        });

        doc.save(function (err, doc) {
          if(err) return cb(err);
          issue._id = doc._id;

          cb(null, issue);

          // UPDATE VOTE AFTER SENDING BACK THE CALLBACK
          // this doesn't need happen before a user gets the information back,
          // it can happen in the background
          castFirstIssueVote(doc, issue.user.login);
        });
      }
    }, callback);
  });
}

// cast a vote for an issue if the author is a user of pullup
function castFirstIssueVote(issue, author_username) {

  // if this doc has no poster, it's author was not a member the last time this check was run
  if(issue.poster) return;

  User.findOne({
    username: author_username
  }, function (err, user) {

    if(err) return console.warn(err);

    // only do anything if the user has since registered
    if(user) {

      var vote = new Vote({
        item: issue,
        voter: user,
        amount: 1,
        itemType: 'issue'
      });

      vote.save(function (err, vote) {
        if(err) {
          // 11000 means someone else already got to this before we could, not a big deal
          return console.warn(err);
        }

        // add the user as the poster of the issue
        issue.poster = user;

        issue.save(function (err) {
          if(err) return console.warn(err);
        });
      });

    }
  });
}

function sortIssues(issues, user, callback) {

  addVotesToIssues(issues, user, function (err, issues) {
    if(err) return callback(err);

    // sort by vote count and then age
    issues.sort(function (a, b) {
      return (b.votes - a.votes) || (new Date(b.created_at) - new Date(a.created_at));
    });

    callback(null, issues);
  });
}
<commit_msg>add github authentication ot request<commit_after>var GitHubApi = require('github');
var Issue = require('../models/Issue');
var User = require('../models/User');
var Vote = require('../models/Vote');
var async = require('async');
var votesController = require('./votes');
var addVotesToIssues = votesController.addVotesFor('issue');
var githubSecrets = require('../config/secrets').github;
var github = new GitHubApi({
  version: "3.0.0"
});

function githubToken(user) {
  if(!user || !user.tokens || !user.tokens.length) return false;

  for(var i=0; i<user.tokens.length; i++) {
    if(!user.tokens[i]) continue;
    if(user.tokens[i].kind === 'github') return user.tokens[i].accessToken;
  }

  return false;
}

/**
 * GET /issues
 * View all open issues in the project
 */
exports.index = function (req, res, next) {
  var token = githubToken(req.user);

  if(token) {
    // authenticate using the logged in user
    github.authenticate({
      type: 'oauth',
      token: token
    });
  } else {
    // authenticate using the app's credentials
    github.authenticate({
        type: "oauth",
        key: githubSecrets.clientID,
        secret: githubSecrets.clientSecret
    });
  }

  github.issues.repoIssues({
    user: 'larvalabs',
    repo: 'pullup',
    state: 'open'
  }, function (err, issues) {
    if(err) return next(err);

    getIssueIds(issues, function (err, issues) {

      if(err) return next(err);

      sortIssues(issues, req.user, function (err, issues) {

        if(err) return next(err);

        res.render('issues/index', {
          title: 'Open Issues',
          issues: issues
        });

      });

    });
  });
};

// get the internal issue `_id`, and create new docs for those that don't yet exist
function getIssueIds(issues, callback) {
  if(!issues || !issues.length) return callback(null, issues);

  Issue
  .find({
    number: {
      $in: issues.map(function (issue) {
        return issue.number;
      })
    }
  })
  .exec(function (err, docs) {
    if(err) return callback(err);

    var docsByNumber = {};

    docs.forEach(function (doc) {
      docsByNumber[doc.number] = doc;
    });

    async.map(issues, function (issue, cb) {
      var doc = docsByNumber[issue.number];

      if(doc) {
        // this issue has been created in our db already
        issue._id = doc._id;
        cb(null, issue);

        // UPDATE VOTE AFTER SENDING BACK THE CALLBACK
        // this doesn't need happen before a user gets the information back,
        // it can happen in the background
        castFirstIssueVote(doc, issue.user.login);

      } else {
        // we need to create a new issue
        doc = new Issue({
          number: issue.number
        });

        doc.save(function (err, doc) {
          if(err) return cb(err);
          issue._id = doc._id;

          cb(null, issue);

          // UPDATE VOTE AFTER SENDING BACK THE CALLBACK
          // this doesn't need happen before a user gets the information back,
          // it can happen in the background
          castFirstIssueVote(doc, issue.user.login);
        });
      }
    }, callback);
  });
}

// cast a vote for an issue if the author is a user of pullup
function castFirstIssueVote(issue, author_username) {

  // if this doc has no poster, it's author was not a member the last time this check was run
  if(issue.poster) return;

  User.findOne({
    username: author_username
  }, function (err, user) {

    if(err) return console.warn(err);

    // only do anything if the user has since registered
    if(user) {

      var vote = new Vote({
        item: issue,
        voter: user,
        amount: 1,
        itemType: 'issue'
      });

      vote.save(function (err, vote) {
        if(err) {
          // 11000 means someone else already got to this before we could, not a big deal
          return console.warn(err);
        }

        // add the user as the poster of the issue
        issue.poster = user;

        issue.save(function (err) {
          if(err) return console.warn(err);
        });
      });

    }
  });
}

function sortIssues(issues, user, callback) {

  addVotesToIssues(issues, user, function (err, issues) {
    if(err) return callback(err);

    // sort by vote count and then age
    issues.sort(function (a, b) {
      return (b.votes - a.votes) || (new Date(b.created_at) - new Date(a.created_at));
    });

    callback(null, issues);
  });
}
<|endoftext|>


<commit_before>// Simple file to simulate a webhook
var local_path =  (require.resolve ("./cloud.js")).replace ("cloud.js", "");
var config = require (local_path + ".app-config.json") || {};
var https = require ("https");
var http = require ("http");
var cloud = require ("./cloud.js");
var async = require ("async");

// Pass in the instance data that you want to post to, should be in the form of a json string
var instance_data = process.argv[2];
instance_data = '{"type":"production", "listensTo": "master", "deploy":true}';
try {instance_data = JSON.parse (instance_data);}
catch (err){}

// post a command out
function post (url, body, port, secure, path, cb){
    var qs = require('querystring');
    body = qs.stringify (body);
    var options = {
        host: url,
        port: port,
        path: path,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': body.length
        },
        rejectUnauthorized:false // add this so if someone uses a local certificate on the machine the post will still go through
    };

    var server = (secure) ? https : http;
    var req = server.request(options, function(res) {
        var result = "";
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            result += chunk;
        });
        res.on('end', function (){
            cb (null, result);
        });
    });
    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        cb (e.message);
    });

    // write data to request body
    req.write(body);
    req.end();
    console.log ("posted to " + url + " waiting for response....");
}

// go get all the instances and post to them
cloud.init (config, function (){
    var body = {};
    var key = config.pullField || "ref";
    var branch = config.pullBranch || instance_data.listensTo;
    body[key] = branch;
    var secure = (config.appURL.search ("https://") !== -1);
    var path = "/pull?";
    if (config.pullSecret) {path += "secret=" + config.pullSecret + "&";}
    path += "master=false";

    if (instance_data){
        cloud.setInstanceData (instance_data);
        cloud.getInstances (function (err, instances){
            if (instances && instances.length){
                console.log ("Found " + instances.length + " instances, posting pull request.");
                async.map (instances, function (instance, cb){
                    post (instance.dns, body, config.pullPort, secure, path, function (err, result){
                        console.log ("\nPosted pull to:" + instance.dns);
                        console.log ("\tport:" + config.pullPort);
                        console.log ("\tsecure:" + secure);
                        console.log ("\tpath:" + path);
                        console.log ("\tbody:" + body);
                        if (err){console.log ("\n\tError:" + err);}
                        else{console.log ("\n\tResult:" + result);}
                        cb ();
                    });
                }, function () {process.exit (0);});
            }
            else{
                console.log ("No instances found");
                process.exit (0);
            }
        });
    }
    else{
        console.log ("Error posting to instances, instance data json string no passed in or corrupt");
        process.exit (0);
    }
});
<commit_msg>IMproved support for signalling the servers<commit_after>// Simple file to simulate a webhook
var local_path =  (require.resolve ("./cloud.js")).replace ("cloud.js", "");
var config = require (local_path + ".app-config.json") || {};
var https = require ("https");
var http = require ("http");
var cloud = require ("./cloud.js");
var async = require ("async");

// Pass in the instance data that you want to post to, should be in the form of a json string
var instance_data = process.argv[2];
try {instance_data = JSON.parse (instance_data);}
catch (err){}

// post a command out
function post (url, body, port, secure, path, cb){
    var qs = require('querystring');
    body = qs.stringify (body);
    var options = {
        host: url,
        port: port,
        path: path,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': body.length
        },
        rejectUnauthorized:false // add this so if someone uses a local certificate on the machine the post will still go through
    };

    var server = (secure) ? https : http;
    var req = server.request(options, function(res) {
        var result = "";
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            result += chunk;
        });
        res.on('end', function (){
            cb (null, result);
        });
    });
    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        cb (e.message);
    });

    // write data to request body
    req.write(body);
    req.end();
    console.log ("posted to " + url + " waiting for response....");
}

// go get all the instances and post to them
cloud.init (config, function (){
    var body = {};
    var key = config.pullField || "ref";
    var branch = config.pullBranch || instance_data.listensTo;
    body[key] = branch;
    var secure = (config.appURL.search ("https://") !== -1);
    var path = "/pull?";
    if (config.pullSecret) {path += "secret=" + config.pullSecret + "&";}
    path += "master=false";

    if (instance_data){
        cloud.setInstanceData (instance_data);
        cloud.getInstances (function (err, instances){
            if (instances && instances.length){
                console.log ("Found " + instances.length + " instances, posting pull request.");
                async.map (instances, function (instance, cb){
                    post (instance.dns, body, config.pullPort, secure, path, function (err, result){
                        console.log ("\nPosted pull to:" + instance.dns);
                        console.log ("\tport:" + config.pullPort);
                        console.log ("\tsecure:" + secure);
                        console.log ("\tpath:" + path);
                        console.log ("\tbody:" + body);
                        if (err){console.log ("\n\tError:" + err);}
                        else{console.log ("\n\tResult:" + result);}
                        cb ();
                    });
                }, function () {process.exit (0);});
            }
            else{
                console.log ("No instances found");
                process.exit (0);
            }
        });
    }
    else{
        console.log ("Error posting to instances, instance data json string no passed in or corrupt");
        process.exit (0);
    }
});
<|endoftext|>


<commit_before>
    angular.module('lux.grid', ['ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection'])

        // Directive to build Angular-UI grid options using Lux REST API
        .directive('restGrid', ['$lux', '$window', 'uiGridConstants', function ($lux, $window, uiGridConstants) {

            var paginationSize = 25;

            // Get initial data
            function getInitialData (scope, options) {

                var api = $lux.api(options.target);

                api.get({path: '/metadata'}).success(function(resp) {
                    paginationSize = resp['default-limit'];

                    api.get({}, {limit:paginationSize}).success(function(resp) {
                        scope.gridOptions.totalItems = resp.total;
                        scope.gridOptions.data = resp.result;
                    });
                });
            }

            // Get specified page
            function getPage (scope, api, pageNumber) {

                var params = {
                    limit: paginationSize,
                    offset: paginationSize*(pageNumber - 1)
                };

                api.get({}, params).success(function(resp) {
                    scope.gridOptions.totalItems = resp.total;
                    scope.gridOptions.data = resp.result;
                });
            }

            // Get current page using query string
            function getPageByParams(scope, api, params) {
                api.get({}, params).success(function(resp) {
                    scope.gridOptions.data = resp.result;
                });
            }

            // Pre-process grid options
            function buildOptions (scope, options) {

                scope.objectUrl = function(objectId) {
                    return $window.location + '/' + objectId;
                };

                function getColumnType(type) {
                    switch (type) {
                        case 'integer':     return 'number';
                        case 'datetime':    return 'date';
                        default:            return type;
                    }
                }

                var api = $lux.api(options.target),
                    columns = [],
                    gridOptions = {
                        paginationPageSizes: [paginationSize],
                        paginationPageSize: paginationSize,
                        useExternalPagination: true,
                        useExternalSorting: true,
                        enableFiltering: true,
                        columnDefs: [],
                        rowHeight: 30,
                        onRegisterApi: function(gridApi) {
                            scope.gridApi = gridApi;
                            scope.gridApi.pagination.on.paginationChanged(scope, function(currentPage) {
                                getPage(scope, api, currentPage);
                            });

                            scope.gridApi.core.on.sortChanged(scope, function(grid, sortColumns) {
                                var params = {};

                                // Build query string for sorting
                                angular.forEach(sortColumns, function(column) {
                                    params.sortby = column.name + ':' + column.sort.direction;
                                });

                                if( sortColumns.length === 0)
                                    getPageByParams(scope, api, params);
                                else {
                                    switch( sortColumns[0].sort.direction ) {
                                        case uiGridConstants.ASC:
                                            getPageByParams(scope, api, params);
                                            break;
                                        case uiGridConstants.DESC:
                                            getPageByParams(scope, api, params);
                                            break;
                                        case undefined:
                                            getPageByParams(scope, api, params);
                                            break;
                                    }
                                }
                            });

                            angular.forEach(options.columns, function(col) {
                                var column = {
                                    field: col.field,
                                    displayName: col.displayName,
                                    type: getColumnType(col.type),
                                    enableSorting: col.sortable,
                                    enableFiltering: col.filter,
                                };

                                if (column.field === 'id')
                                    column.cellTemplate = '
{{COL_FIELD}}
';

                                if (column.type === 'date') {
                                    column.sortingAlgorithm = function(a, b) {
                                        var dt1 = new Date(a).getTime(),
                                            dt2 = new Date(b).getTime();
                                        return dt1 === dt2 ? 0 : (dt1 < dt2 ? -1 : 1);
                                    };
                                } else if (column.type === 'boolean') {
                                    column.cellTemplate = '

';

                                    column.filter = {
                                        type: uiGridConstants.filter.SELECT,
                                        selectOptions: [{ value: 'true', label: 'True' }, { value: 'false', label: 'False'}],
                                    };
                                }

                                gridOptions.columnDefs.push(column);
                            });
                        }
                    };

                return gridOptions;
            }

            return {
                restrict: 'A',
                link: {
                    pre: function (scope, element, attrs) {
                        var scripts= element[0].getElementsByTagName('script');

                        forEach(scripts, function (js) {
                            globalEval(js.innerHTML);
                        });

                        var opts = attrs;
                        if (attrs.restGrid) opts = {options: attrs.restGrid};

                        opts = getOptions(opts);

                        if (opts)
                            scope.gridOptions = buildOptions(scope, opts);

                        getInitialData(scope, opts);
                    },
                },
            };

        }]);
<commit_msg>Added support to grid for change page sizes (25, 50, 100)<commit_after>
    angular.module('lux.grid', ['ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection'])

        // Directive to build Angular-UI grid options using Lux REST API
        .directive('restGrid', ['$lux', '$window', 'uiGridConstants', function ($lux, $window, uiGridConstants) {

            var paginationOptions = {
                limit: 25,
                sizes: [25, 50, 100],
            };

            // Get initial data
            function getInitialData (scope, options) {

                var api = $lux.api(options.target);

                api.get({path: '/metadata'}).success(function(resp) {
                    paginationOptions.limit = resp['default-limit'];

                    api.get({}, {limit: paginationOptions.limit}).success(function(resp) {
                        scope.gridOptions.totalItems = resp.total;
                        scope.gridOptions.data = resp.result;
                    });
                });
            }

            // Get specified page
            function getPage (scope, api, pageNumber, pageSize) {

                var params = {
                    limit: pageSize,
                    offset: pageSize*(pageNumber - 1)
                };

                api.get({}, params).success(function(resp) {
                    scope.gridOptions.totalItems = resp.total;
                    scope.gridOptions.data = resp.result;
                });
            }

            // Get current page using query string
            function getPageByParams(scope, api, params) {
                api.get({}, params).success(function(resp) {
                    scope.gridOptions.data = resp.result;
                });
            }

            // Pre-process grid options
            function buildOptions (scope, options) {

                scope.objectUrl = function(objectId) {
                    return $window.location + '/' + objectId;
                };

                function getColumnType(type) {
                    switch (type) {
                        case 'integer':     return 'number';
                        case 'datetime':    return 'date';
                        default:            return type;
                    }
                }

                var api = $lux.api(options.target),
                    columns = [],
                    gridOptions = {
                        paginationPageSizes: paginationOptions.sizes,
                        paginationPageSize: paginationOptions.limit,
                        useExternalPagination: true,
                        useExternalSorting: true,
                        enableFiltering: true,
                        columnDefs: [],
                        rowHeight: 30,
                        onRegisterApi: function(gridApi) {
                            scope.gridApi = gridApi;
                            scope.gridApi.pagination.on.paginationChanged(scope, function(pageNumber, pageSize) {
                                getPage(scope, api, pageNumber, pageSize);
                            });

                            scope.gridApi.core.on.sortChanged(scope, function(grid, sortColumns) {
                                var params = {};

                                // Build query string for sorting
                                angular.forEach(sortColumns, function(column) {
                                    params.sortby = column.name + ':' + column.sort.direction;
                                });

                                if( sortColumns.length === 0)
                                    getPageByParams(scope, api, params);
                                else {
                                    switch( sortColumns[0].sort.direction ) {
                                        case uiGridConstants.ASC:
                                            getPageByParams(scope, api, params);
                                            break;
                                        case uiGridConstants.DESC:
                                            getPageByParams(scope, api, params);
                                            break;
                                        case undefined:
                                            getPageByParams(scope, api, params);
                                            break;
                                    }
                                }
                            });

                            angular.forEach(options.columns, function(col) {
                                var column = {
                                    field: col.field,
                                    displayName: col.displayName,
                                    type: getColumnType(col.type),
                                    enableSorting: col.sortable,
                                    enableFiltering: col.filter,
                                };

                                if (column.field === 'id')
                                    column.cellTemplate = '
{{COL_FIELD}}
';

                                if (column.type === 'date') {
                                    column.sortingAlgorithm = function(a, b) {
                                        var dt1 = new Date(a).getTime(),
                                            dt2 = new Date(b).getTime();
                                        return dt1 === dt2 ? 0 : (dt1 < dt2 ? -1 : 1);
                                    };
                                } else if (column.type === 'boolean') {
                                    column.cellTemplate = '

';

                                    column.filter = {
                                        type: uiGridConstants.filter.SELECT,
                                        selectOptions: [{ value: 'true', label: 'True' }, { value: 'false', label: 'False'}],
                                    };
                                }

                                gridOptions.columnDefs.push(column);
                            });
                        }
                    };

                return gridOptions;
            }

            return {
                restrict: 'A',
                link: {
                    pre: function (scope, element, attrs) {
                        var scripts= element[0].getElementsByTagName('script');

                        forEach(scripts, function (js) {
                            globalEval(js.innerHTML);
                        });

                        var opts = attrs;
                        if (attrs.restGrid) opts = {options: attrs.restGrid};

                        opts = getOptions(opts);

                        if (opts)
                            scope.gridOptions = buildOptions(scope, opts);

                        getInitialData(scope, opts);
                    },
                },
            };

        }]);
<|endoftext|>


<commit_before>import React from "react";
import get from "lodash.get";
import { Helmet } from "react-helmet-async";
import PropTypes from "prop-types";
import Context from "@times-components/context";
import { renderTreeAsText } from "@times-components/markup-forest";
import { appendToImageURL } from "@times-components/utils";

// Get the section for an article, preferring it not to be News
function getSectionName(article) {
  const { tiles } = article;

  if (!tiles) {
    return null;
  }

  const slices = tiles.reduce((acc, tile) => {
    acc.push(...tile.slices);
    return acc;
  }, []);
  const sections = slices.reduce((acc, slice) => {
    acc.push(...slice.sections);
    return acc;
  }, []);
  const titles = sections.map(section => section.title);

  if (titles.length === 0) {
    return null;
  }

  const nonNews = titles.filter(title => title !== "News");

  return nonNews.length ? nonNews[0] : "News";
}
function getIsLiveBlog(articleFlags = []) {
  if (articleFlags !== undefined) {
    const articleLiveFlag = articleFlags.find(
      flag => flag.type === "LIVE" && Date.now() < new Date(flag.expiryTime)
    );
    return articleLiveFlag !== undefined;
  }
  return false;
}
function getAuthorAsText(article) {
  const { bylines } = article;
  if (!bylines) {
    return null;
  }
  const children = bylines.reduce((acc, byline) => {
    if (Array.isArray(byline.byline)) {
      acc.push(...byline.byline);
    } else {
      acc.push(byline.byline);
    }
    return acc;
  }, []);
  return renderTreeAsText({ children });
}

function getAuthors({ bylines }) {
  return bylines.map(byline => byline.author).filter(author => author);
}

function getAuthorSchema(article) {
  const { bylines } = article;
  return bylines
    ? getAuthors(article).map(({ name, jobTitle, twitter, slug }) => {
        const url = `https://thetimes.co.uk/profile/${slug}`;
        return {
          "@type": "Person",
          name,
          jobTitle,
          sameAs: twitter ? [url, `https://twitter.com/${twitter}`] : url
        };
      })
    : [];
}

const PUBLICATION_NAMES = {
  SUNDAYTIMES: "The Sunday Times",
  TIMES: "The Times"
};

const get169CropUrl = asset => get(asset, "crop169.url", null);

const getVideoLeadAssetUrl = article =>
  get169CropUrl(
    get(article, "leadAsset.posterImage", get(article, "leadAsset", null))
  );

const getImageLeadAssetUrl = article =>
  get169CropUrl(get(article, "leadAsset", null));

const getArticleLeadAssetUrl = article =>
  (article.hasVideo ? getVideoLeadAssetUrl : getImageLeadAssetUrl)(article);

const getThumbnailUrlFromImage = article => {
  const tileUrl =
    article.tiles &&
    article.tiles.find(tile => get(tile.leadAsset, "crop169.url", null));

  if (tileUrl) {
    return tileUrl;
  }

  const listingAssetUrl = get(article.listingAsset, "crop169.url", null);

  if (listingAssetUrl) {
    return listingAssetUrl;
  }

  return get(article.leadAsset, "crop169.url", null);
};

const getThumbnailUrl = article => {
  const { hasVideo, leadAsset } = article;
  const thumbnailUrl = hasVideo
    ? getVideoLeadAssetUrl(article)
    : getThumbnailUrlFromImage(article);

  if (thumbnailUrl) return thumbnailUrl;

  if (!leadAsset) return null;

  const { crop32, crop1251, crop11, crop45, crop23, crop2251 } =
    leadAsset && leadAsset.posterImage ? leadAsset.posterImage : leadAsset;
  const crop = crop32 || crop1251 || crop11 || crop45 || crop23 || crop2251;
  return crop ? crop.url : "";
};

function Head({
  article,
  logoUrl,
  paidContentClassName,
  getFallbackThumbnailUrl169,
  swgProductId
}) {
  const {
    descriptionMarkup,
    headline,
    leadAsset,
    publicationName,
    shortHeadline,
    publishedTime,
    updatedTime,
    hasVideo,
    seoDescription,
    keywords,
    url
  } = article;

  const isLiveBlogArticle = getIsLiveBlog(article.expirableFlags);
  const publication = PUBLICATION_NAMES[publicationName];
  const authorName = getAuthorAsText(article);
  const desc =
    seoDescription ||
    (Array.isArray(descriptionMarkup) && descriptionMarkup.length
      ? renderTreeAsText({ children: descriptionMarkup })
      : null);
  const sectionname = getSectionName(article);
  const thumbnailUrl =
    getThumbnailUrl(article) ||
    (getFallbackThumbnailUrl169 ? getFallbackThumbnailUrl169() : null);

  const leadassetUrl =
    appendToImageURL(getArticleLeadAssetUrl(article), "resize", 1200) ||
    thumbnailUrl;
  const authors = getAuthorSchema(article);
  const caption = get(leadAsset, "caption", null);
  const title = headline || shortHeadline || "";
  const datePublished = publishedTime && new Date(publishedTime).toISOString();

  const dateModified = updatedTime || datePublished;

  const defaultAuthorSchema = {
    "@type": "Organization",
    name: "The Times"
  };
  const textByLineAuthorSchema = authorName
    ? { "@type": "Person", name: authorName }
    : null;
  const authorSchema =
    (authors && authors.length ? authors : textByLineAuthorSchema) ||
    defaultAuthorSchema;

  const jsonLD = {
    "@context": "https://schema.org",
    "@type": "NewsArticle",
    headline: title,
    publisher: {
      "@type": "Organization",
      name: publication,
      logo: {
        "@type": "ImageObject",
        url: logoUrl
      }
    },
    mainEntityOfPage: {
      "@type": "WebPage"
    },
    dateCreated: publishedTime,
    datePublished,
    isAccessibleForFree: false,
    hasPart: {
      "@type": "WebPageElement",
      isAccessibleForFree: false,
      cssSelector: `.${paidContentClassName}`
    },
    image: {
      "@type": "ImageObject",
      url: leadassetUrl,
      caption
    },
    thumbnailUrl,
    dateModified,
    author: authorSchema
  };

  if (swgProductId) {
    jsonLD.isPartOf = {
      "@type": ["CreativeWork", "Product"],
      name: "The Times & The Sunday Times",
      productID: swgProductId
    };
  }
  const videoJsonLD = hasVideo
    ? {
        "@context": "https://schema.org",
        "@type": "VideoObject",
        name: leadAsset.title || title,
        uploadDate: dateModified,
        thumbnailUrl,
        description:
          Array.isArray(descriptionMarkup) && descriptionMarkup.length
            ? renderTreeAsText({ children: descriptionMarkup })
            : seoDescription || leadAsset.title || title,
        contentUrl: url
      }
    : null;

  const liveBlogJsonLD = {
    "@context": "https://schema.org",
    "@type": "LiveBlogPosting",
    headline,
    description: seoDescription,
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": url
    },
    datePublished: publishedTime,
    dateModified: updatedTime,
    coverageStartTime: publishedTime,
    url,
    keywords,
    image: {
      "@type": "ImageObject",
      url: leadassetUrl,
      caption
    },
    publisher: {
      "@type": "Organization",
      name: publication,
      logo: {
        "@type": "ImageObject",
        url: logoUrl
      }
    },
    author: authorSchema
    // when the liveBlogUpdate has some real data, then you can use this structure to populate the updates
    /** * /
    ,
    "liveBlogUpdate":[
      {
        "type":"BlogPosting",
        "headline":"This is a headline. The apocalypse is here",
        "datePublished":"1999-12-31T23:59:59Z",
        "url": "https://schema.org/LiveBlogPosting"
      }
    ]
    /** */
  };
  return (
    <Context.Consumer>
      {({ makeArticleUrl }) => {
        jsonLD.mainEntityOfPage["@id"] = makeArticleUrl(article);
        return (
          <Helmet encodeSpecialCharacters={false}>
            <title>
              {title} | {sectionname ? `${sectionname} | ` : ""}
              {publication}
            </title>
            <meta name="robots" content="max-image-preview:large" />
            <meta content={title} name="article:title" />
            <meta content={publication} name="article:publication" />
            {desc && <meta content={desc} name="description" />}
            {authorName && <meta content={authorName} name="author" />}

            <meta content={title} property="og:title" />
            <meta content="article" property="og:type" />
            <meta content={makeArticleUrl(article)} property="og:url" />
            {desc && <meta content={desc} property="og:description" />}
            {leadassetUrl && (
              <meta content={leadassetUrl} property="og:image" />
            )}
            {hasVideo && <meta name="robots" content="max-video-preview:-1" />}

            <meta content={title} name="twitter:title" />
            <meta content="summary_large_image" name="twitter:card" />
            <meta content={makeArticleUrl(article)} name="twitter:url" />
            {desc && <meta content={desc} name="twitter:description" />}
            {leadassetUrl && (
              <meta content={leadassetUrl} name="twitter:image" />
            )}

            {isLiveBlogArticle ? (
              <script type="application/ld+json">
                {JSON.stringify(liveBlogJsonLD)}
              </script>
            ) : (
              <script type="application/ld+json">
                {JSON.stringify(jsonLD)}
              </script>
            )}

            {videoJsonLD && (
              <script type="application/ld+json">
                {JSON.stringify(videoJsonLD)}
              </script>
            )}
          </Helmet>
        );
      }}
    </Context.Consumer>
  );
}

Head.propTypes = {
  article: PropTypes.shape({
    bylines: PropTypes.array,
    descriptionMarkup: PropTypes.array,
    headline: PropTypes.string,
    id: PropTypes.string.isRequired,
    leadAsset: PropTypes.object,
    publicationName: PropTypes.string.isRequired,
    shortHeadline: PropTypes.string,
    shortIdentifier: PropTypes.string.isRequired,
    tiles: PropTypes.array
  }).isRequired,
  logoUrl: PropTypes.string.isRequired,
  paidContentClassName: PropTypes.string.isRequired,
  getFallbackThumbnailUrl169: PropTypes.func.isRequired,
  swgProductId: PropTypes.string
};

Head.defaultProps = {
  swgProductId: null
};

export default Head;
<commit_msg>feat(TDP-758): updated validation of expiry time to include null for open ended validity (#2857)<commit_after>import React from "react";
import get from "lodash.get";
import { Helmet } from "react-helmet-async";
import PropTypes from "prop-types";
import Context from "@times-components/context";
import { renderTreeAsText } from "@times-components/markup-forest";
import { appendToImageURL } from "@times-components/utils";

// Get the section for an article, preferring it not to be News
function getSectionName(article) {
  const { tiles } = article;

  if (!tiles) {
    return null;
  }

  const slices = tiles.reduce((acc, tile) => {
    acc.push(...tile.slices);
    return acc;
  }, []);
  const sections = slices.reduce((acc, slice) => {
    acc.push(...slice.sections);
    return acc;
  }, []);
  const titles = sections.map(section => section.title);

  if (titles.length === 0) {
    return null;
  }

  const nonNews = titles.filter(title => title !== "News");

  return nonNews.length ? nonNews[0] : "News";
}
function getIsLiveBlog(articleFlags = []) {
  if (articleFlags !== undefined) {
    const articleLiveFlag = articleFlags.find(
      flag =>
        flag.type === "LIVE" &&
        (Date.now() < new Date(flag.expiryTime) || flag.expiryTime === null)
    );
    return articleLiveFlag !== undefined;
  }
  return false;
}
function getAuthorAsText(article) {
  const { bylines } = article;
  if (!bylines) {
    return null;
  }
  const children = bylines.reduce((acc, byline) => {
    if (Array.isArray(byline.byline)) {
      acc.push(...byline.byline);
    } else {
      acc.push(byline.byline);
    }
    return acc;
  }, []);
  return renderTreeAsText({ children });
}

function getAuthors({ bylines }) {
  return bylines.map(byline => byline.author).filter(author => author);
}

function getAuthorSchema(article) {
  const { bylines } = article;
  return bylines
    ? getAuthors(article).map(({ name, jobTitle, twitter, slug }) => {
        const url = `https://thetimes.co.uk/profile/${slug}`;
        return {
          "@type": "Person",
          name,
          jobTitle,
          sameAs: twitter ? [url, `https://twitter.com/${twitter}`] : url
        };
      })
    : [];
}

const PUBLICATION_NAMES = {
  SUNDAYTIMES: "The Sunday Times",
  TIMES: "The Times"
};

const get169CropUrl = asset => get(asset, "crop169.url", null);

const getVideoLeadAssetUrl = article =>
  get169CropUrl(
    get(article, "leadAsset.posterImage", get(article, "leadAsset", null))
  );

const getImageLeadAssetUrl = article =>
  get169CropUrl(get(article, "leadAsset", null));

const getArticleLeadAssetUrl = article =>
  (article.hasVideo ? getVideoLeadAssetUrl : getImageLeadAssetUrl)(article);

const getThumbnailUrlFromImage = article => {
  const tileUrl =
    article.tiles &&
    article.tiles.find(tile => get(tile.leadAsset, "crop169.url", null));

  if (tileUrl) {
    return tileUrl;
  }

  const listingAssetUrl = get(article.listingAsset, "crop169.url", null);

  if (listingAssetUrl) {
    return listingAssetUrl;
  }

  return get(article.leadAsset, "crop169.url", null);
};

const getThumbnailUrl = article => {
  const { hasVideo, leadAsset } = article;
  const thumbnailUrl = hasVideo
    ? getVideoLeadAssetUrl(article)
    : getThumbnailUrlFromImage(article);

  if (thumbnailUrl) return thumbnailUrl;

  if (!leadAsset) return null;

  const { crop32, crop1251, crop11, crop45, crop23, crop2251 } =
    leadAsset && leadAsset.posterImage ? leadAsset.posterImage : leadAsset;
  const crop = crop32 || crop1251 || crop11 || crop45 || crop23 || crop2251;
  return crop ? crop.url : "";
};

function Head({
  article,
  logoUrl,
  paidContentClassName,
  getFallbackThumbnailUrl169,
  swgProductId
}) {
  const {
    descriptionMarkup,
    headline,
    leadAsset,
    publicationName,
    shortHeadline,
    publishedTime,
    updatedTime,
    hasVideo,
    seoDescription,
    keywords,
    url
  } = article;

  const isLiveBlogArticle = getIsLiveBlog(article.expirableFlags);
  const publication = PUBLICATION_NAMES[publicationName];
  const authorName = getAuthorAsText(article);
  const desc =
    seoDescription ||
    (Array.isArray(descriptionMarkup) && descriptionMarkup.length
      ? renderTreeAsText({ children: descriptionMarkup })
      : null);
  const sectionname = getSectionName(article);
  const thumbnailUrl =
    getThumbnailUrl(article) ||
    (getFallbackThumbnailUrl169 ? getFallbackThumbnailUrl169() : null);

  const leadassetUrl =
    appendToImageURL(getArticleLeadAssetUrl(article), "resize", 1200) ||
    thumbnailUrl;
  const authors = getAuthorSchema(article);
  const caption = get(leadAsset, "caption", null);
  const title = headline || shortHeadline || "";
  const datePublished = publishedTime && new Date(publishedTime).toISOString();

  const dateModified = updatedTime || datePublished;

  const defaultAuthorSchema = {
    "@type": "Organization",
    name: "The Times"
  };
  const textByLineAuthorSchema = authorName
    ? { "@type": "Person", name: authorName }
    : null;
  const authorSchema =
    (authors && authors.length ? authors : textByLineAuthorSchema) ||
    defaultAuthorSchema;

  const jsonLD = {
    "@context": "https://schema.org",
    "@type": "NewsArticle",
    headline: title,
    publisher: {
      "@type": "Organization",
      name: publication,
      logo: {
        "@type": "ImageObject",
        url: logoUrl
      }
    },
    mainEntityOfPage: {
      "@type": "WebPage"
    },
    dateCreated: publishedTime,
    datePublished,
    isAccessibleForFree: false,
    hasPart: {
      "@type": "WebPageElement",
      isAccessibleForFree: false,
      cssSelector: `.${paidContentClassName}`
    },
    image: {
      "@type": "ImageObject",
      url: leadassetUrl,
      caption
    },
    thumbnailUrl,
    dateModified,
    author: authorSchema
  };

  if (swgProductId) {
    jsonLD.isPartOf = {
      "@type": ["CreativeWork", "Product"],
      name: "The Times & The Sunday Times",
      productID: swgProductId
    };
  }
  const videoJsonLD = hasVideo
    ? {
        "@context": "https://schema.org",
        "@type": "VideoObject",
        name: leadAsset.title || title,
        uploadDate: dateModified,
        thumbnailUrl,
        description:
          Array.isArray(descriptionMarkup) && descriptionMarkup.length
            ? renderTreeAsText({ children: descriptionMarkup })
            : seoDescription || leadAsset.title || title,
        contentUrl: url
      }
    : null;

  const liveBlogJsonLD = {
    "@context": "https://schema.org",
    "@type": "LiveBlogPosting",
    headline,
    description: seoDescription,
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": url
    },
    datePublished: publishedTime,
    dateModified: updatedTime,
    coverageStartTime: publishedTime,
    url,
    keywords,
    image: {
      "@type": "ImageObject",
      url: leadassetUrl,
      caption
    },
    publisher: {
      "@type": "Organization",
      name: publication,
      logo: {
        "@type": "ImageObject",
        url: logoUrl
      }
    },
    author: authorSchema
    // when the liveBlogUpdate has some real data, then you can use this structure to populate the updates
    /** * /
    ,
    "liveBlogUpdate":[
      {
        "type":"BlogPosting",
        "headline":"This is a headline. The apocalypse is here",
        "datePublished":"1999-12-31T23:59:59Z",
        "url": "https://schema.org/LiveBlogPosting"
      }
    ]
    /** */
  };
  return (
    <Context.Consumer>
      {({ makeArticleUrl }) => {
        jsonLD.mainEntityOfPage["@id"] = makeArticleUrl(article);
        return (
          <Helmet encodeSpecialCharacters={false}>
            <title>
              {title} | {sectionname ? `${sectionname} | ` : ""}
              {publication}
            </title>
            <meta name="robots" content="max-image-preview:large" />
            <meta content={title} name="article:title" />
            <meta content={publication} name="article:publication" />
            {desc && <meta content={desc} name="description" />}
            {authorName && <meta content={authorName} name="author" />}

            <meta content={title} property="og:title" />
            <meta content="article" property="og:type" />
            <meta content={makeArticleUrl(article)} property="og:url" />
            {desc && <meta content={desc} property="og:description" />}
            {leadassetUrl && (
              <meta content={leadassetUrl} property="og:image" />
            )}
            {hasVideo && <meta name="robots" content="max-video-preview:-1" />}

            <meta content={title} name="twitter:title" />
            <meta content="summary_large_image" name="twitter:card" />
            <meta content={makeArticleUrl(article)} name="twitter:url" />
            {desc && <meta content={desc} name="twitter:description" />}
            {leadassetUrl && (
              <meta content={leadassetUrl} name="twitter:image" />
            )}

            {isLiveBlogArticle ? (
              <script type="application/ld+json">
                {JSON.stringify(liveBlogJsonLD)}
              </script>
            ) : (
              <script type="application/ld+json">
                {JSON.stringify(jsonLD)}
              </script>
            )}

            {videoJsonLD && (
              <script type="application/ld+json">
                {JSON.stringify(videoJsonLD)}
              </script>
            )}
          </Helmet>
        );
      }}
    </Context.Consumer>
  );
}

Head.propTypes = {
  article: PropTypes.shape({
    bylines: PropTypes.array,
    descriptionMarkup: PropTypes.array,
    headline: PropTypes.string,
    id: PropTypes.string.isRequired,
    leadAsset: PropTypes.object,
    publicationName: PropTypes.string.isRequired,
    shortHeadline: PropTypes.string,
    shortIdentifier: PropTypes.string.isRequired,
    tiles: PropTypes.array
  }).isRequired,
  logoUrl: PropTypes.string.isRequired,
  paidContentClassName: PropTypes.string.isRequired,
  getFallbackThumbnailUrl169: PropTypes.func.isRequired,
  swgProductId: PropTypes.string
};

Head.defaultProps = {
  swgProductId: null
};

export default Head;
<|endoftext|>


<commit_before><commit_msg>update to the multi-resource arrows. accounts for current resource not in view.<commit_after><|endoftext|>