code:
<project name="Cocos2d-xToLua" default="create-lua-bindings" basedir=".">
    <description>
        Utilises tolua++ to create the lua bindings to expose cocos2d-x functionality to lua
    </description>
    <property name="tolua++" location="tolua++.exe"/>

  <target name="cocos2d-x-bindings">
    <echo message="Calling tolua++" />
    <exec executable="${tolua++}">
      <arg value="-L" />
      <arg value="basic.lua" />
      <arg value="-o"/>
      <arg value="LuaCocos2d.cpp"/>
      <arg value="Cocos2d.pkg"/>
    </exec>
    <echo message="Patching resultant lua bindings" />

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[tolua_pushusertype_ccobject(tolua_S,(void*)tolua_ret]]></replacetoken>
      <replacevalue><![CDATA[int nID = (tolua_ret) ? tolua_ret->m_uID : -1;
    int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
    tolua_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[unsigned void* tolua_ret = (unsigned void*)  self->getTiles();]]></replacetoken>
      <replacevalue><![CDATA[unsigned int* tolua_ret = (unsigned int*)  self->getTiles();]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[ccColor3B color = *((ccColor3B*)  tolua_tousertype(tolua_S,4,(void*)&(const ccColor3B)ccBLACK));]]></replacetoken>
      <replacevalue><![CDATA[ccColor3B clr = ccBLACK;
  ccColor3B color = *((ccColor3B*)  tolua_tousertype(tolua_S,4,(void*)&clr));]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA["LUA_FUNCTION",0,&tolua_err]]></replacetoken>
      <replacevalue><![CDATA[&tolua_err]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[LUA_FUNCTION nHandler = *((LUA_FUNCTION*)  tolua_ref_function(tolua_S,]]></replacetoken>
      <replacevalue><![CDATA[int nHandler = (tolua_ref_function(tolua_S,]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[#ifndef __cplusplus
#include "stdlib.h"
#endif
#include "string.h"

#include "tolua++.h"

/* Exported function */
TOLUA_API int  tolua_Cocos2d_open (lua_State* tolua_S);]]></replacetoken>
      <replacevalue><![CDATA[#include <vector>
#include <map>
#include "LuaCocos2d.h"

extern "C" {
#include "tolua_fix.h"
}

#include "cocos2d.h"
#include "CCConfiguration.h"
#include "CCRibbon.h"
#include "SimpleAudioEngine.h"

using namespace cocos2d;
using namespace CocosDenshion;
]]></replacevalue>
    </replace>

    <echo message="Copying new bindings to target location" />
    <move file="LuaCocos2d.cpp" todir="../../lua/cocos2dx_support/"/>
  </target>
  <target name="create-lua-bindings" depends="cocos2d-x-bindings" />
</project>


code:
<project name="Cocos2d-xToLua" default="create-lua-bindings" basedir=".">
    <description>
        Utilises tolua++ to create the lua bindings to expose cocos2d-x functionality to lua
    </description>
    <property name="tolua++" location="/usr/local/bin/tolua++"/>

  <target name="cocos2d-x-bindings">
    <echo message="Calling tolua++" />
    <exec executable="${tolua++}">
      <arg value="-L" />
      <arg value="basic.lua" />
      <arg value="-o"/>
      <arg value="LuaCocos2d.cpp"/>
      <arg value="Cocos2d.pkg"/>
    </exec>
    <echo message="Patching resultant lua bindings" />

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[tolua_pushusertype_ccobject(tolua_S,(void*)tolua_ret]]></replacetoken>
      <replacevalue><![CDATA[int nID = (tolua_ret) ? tolua_ret->m_uID : -1;
    int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
    tolua_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[unsigned void* tolua_ret = (unsigned void*)  self->getTiles();]]></replacetoken>
      <replacevalue><![CDATA[unsigned int* tolua_ret = (unsigned int*)  self->getTiles();]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[ccColor3B color = *((ccColor3B*)  tolua_tousertype(tolua_S,4,(void*)&(const ccColor3B)ccBLACK));]]></replacetoken>
      <replacevalue><![CDATA[ccColor3B clr = ccBLACK;
  ccColor3B color = *((ccColor3B*)  tolua_tousertype(tolua_S,4,(void*)&clr));]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA["LUA_FUNCTION",0,&tolua_err]]></replacetoken>
      <replacevalue><![CDATA[&tolua_err]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[LUA_FUNCTION nHandler = *((LUA_FUNCTION*)  tolua_ref_function(tolua_S,]]></replacetoken>
      <replacevalue><![CDATA[int nHandler = (tolua_ref_function(tolua_S,]]></replacevalue>
    </replace>

    <replace file="LuaCocos2d.cpp">
      <replacetoken><![CDATA[#ifndef __cplusplus
#include "stdlib.h"
#endif
#include "string.h"

#include "tolua++.h"

/* Exported function */
TOLUA_API int  tolua_Cocos2d_open (lua_State* tolua_S);]]></replacetoken>
      <replacevalue><![CDATA[#include <vector>
#include <map>
#include "LuaCocos2d.h"

extern "C" {
#include "tolua_fix.h"
}

#include "cocos2d.h"
#include "CCConfiguration.h"
#include "CCRibbon.h"
#include "SimpleAudioEngine.h"

using namespace cocos2d;
using namespace CocosDenshion;
]]></replacevalue>
    </replace>

    <echo message="Copying new bindings to target location" />
    <move file="LuaCocos2d.cpp" todir="../../lua/cocos2dx_support/"/>
  </target>
  <target name="create-lua-bindings" depends="cocos2d-x-bindings" />
</project>

license: mit


code:
typedef unsigned char    GLubyte;
typedef int              GLint;
typedef int              GLsizei;
typedef unsigned char	 GLubyte;
typedef unsigned short   GLushort;
typedef unsigned int     GLuint;
typedef float			 GLfloat;

typedef float ccTime;
typedef struct _ccColor3B
{
    GLubyte	r;
    GLubyte	g;
    GLubyte b;
} ccColor3B;
ccColor3B ccc3(GLubyte r, GLubyte g, GLubyte b);

typedef struct _ccColor4B
{
    GLubyte r;
    GLubyte g;
    GLubyte b;
    GLubyte a;
} ccColor4B;
//! helper macro that creates an ccColor4B type
static  ccColor4B ccc4f(const GLubyte r, const GLubyte g, const GLubyte b, const GLubyte o);

typedef struct _ccColor4F {
    GLfloat r;
    GLfloat g;
    GLfloat b;
    GLfloat a;
} ccColor4F;
ccColor4F ccc4FFromccc3B(ccColor3B c);
ccColor4F ccc4FFromccc4B(ccColor4B c);
bool ccc4FEqual(ccColor4F a, ccColor4F b);

typedef struct _ccGridSize
{
    int	x;
    int	y;
} ccGridSize;
ccGridSize ccg(int x, int y);

typedef struct _ccBlendFunc
{
    GLenum src;
    GLenum dst;
} ccBlendFunc;

typedef enum
{
    CCTextAlignmentLeft,
    CCTextAlignmentCenter,
    CCTextAlignmentRight,
} CCTextAlignment;


code:
pushd ../../

echo "generating libcocos2dx"
mkdir -p template/xcode4/lib_cocos2dx.xctemplate
python ./tools/xcode4_template_generator/template_generator.py --directory cocos2dx --identifier libcocos2dx --prefix libs --exclude "android win32 airplay wophone bada third_party CCImage.cpp CCThread.cpp CCFileUtils.cpp Android.mk Linux linux qnx marmalade" > ./template/xcode4/lib_cocos2dx.xctemplate/TemplateInfo.plist

echo "generating libcocosdenshion"
mkdir -p template/xcode4/lib_cocosdenshion.xctemplate
python ./tools/xcode4_template_generator/template_generator.py --directory CocosDenshion --identifier libcocosdenshion --prefix libs --exclude "android win32 airplay wophone bada third_party Android.mk Linux linux qnx marmalade" > ./template/xcode4/lib_cocosdenshion.xctemplate/TemplateInfo.plist

echo "generating libbox2d"
mkdir -p template/xcode4/lib_box2d.xctemplate
python ./tools/xcode4_template_generator/template_generator.py --directory Box2D --identifier libbox2d --prefix libs --exclude "android win32 airplay wophone bada Android.mk Linux linux qnx marmalade" > ./template/xcode4/lib_box2d.xctemplate/TemplateInfo.plist

echo "generating libchipmunk"
mkdir -p template/xcode4/lib_chipmunk.xctemplate
python ./tools/xcode4_template_generator/template_generator.py --directory chipmunk --identifier libchipmunk --prefix libs --exclude "android win32 airplay wophone bada Android.mk Linux linux CMakeFiles Makefile qnx marmalade" > ./template/xcode4/lib_chipmunk.xctemplate/TemplateInfo.plist

echo "generating liblua"
mkdir -p template/xcode4/lib_lua.xctemplate
python ./tools/xcode4_template_generator/template_generator.py --directory lua --identifier liblua --prefix libs --append ./tools/xcode4_template_generator/template_lua_patch.txt --exclude "android win32 airplay wophone bada Makefile Linux linux CMakeFiles qnx marmalade" > ./template/xcode4/lib_lua.xctemplate/TemplateInfo.plist

echo "done"

popd


code:
#!/usr/bin/python
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Xcode 4 template generator for cocos2d project
# (c) 2011 Ricardo Quesada
#
# LICENSE: Dual License: MIT & GNU GPL v2 Whatever suits you best.
#
# Given a directory, it generates the "Definitions" and "Nodes" elements
#
# Format taken from: http://blog.boreal-kiss.net/2011/03/11/a-minimal-project-template-for-xcode-4/
# ----------------------------------------------------------------------------
'''
Xcode 4 template generator
'''

__docformat__ = 'restructuredtext'

_template_open_body = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- FILE AUTOGENERATED BY cocos2d-x/tools/template_generator.py DO NOT EDIT -->
<plist version="1.0">
<dict>
	<key>Description</key>
	<string>This is a template description.</string>
	<key>Identifier</key>
	<string>org.cocos2d-x.%s</string>
	<key>Kind</key>
	<string>Xcode.Xcode3.ProjectTemplateUnitKind</string>
"""

_template_close_body = "</dict>\n</plist>"

# python
import sys
import os
import getopt
import glob

class Xcode4Template(object):
    def __init__( self, directory, group=0, identifier="XXX", prefix="libs", exclude=[], append="" ):
        self.directory = directory
        self.files_to_include = []
        self.wildcard = '*'
        self.ignore_extensions = ['h','txt','html','patch']
        self.prefix = prefix
        # self.ignore_keywords = ['win32','airplay','android','Android','wophone', 'CCImage.cpp', 'third_party']
        self.group_start_index = group  # eg: if 1 then libs/cocos2d/support -> ["cocos2d", "support"] ignoring "libs"
        self.output = []
        self.identifier = identifier

    def scandirs(self, path):
        for currentFile in glob.glob( os.path.join(path, self.wildcard) ):
            bExclude = False
            for exclude_keyword in exclude:
                if currentFile.find(exclude_keyword) >= 0:
                    bExclude = True
                    break

            if bExclude is False:
                if os.path.isdir(currentFile):
                    self.scandirs(currentFile)
                else:
                    self.files_to_include.append( currentFile )

    #
    # append the definitions
    #
    def append_definition( self, output_body, path, group, dont_index ):
        #add 'libs/'
        path = self.prefix + '/' + path
        group = [ self.prefix ] + group;

        output_body.append("\t\t<key>%s</key>" % path )

        output_body.append("\t\t<dict>")
        if group:
            output_body.append("\t\t\t<key>Group</key>")
            output_body.append("\t\t\t<array>")
            for g in group:
                output_body.append("\t\t\t\t<string>%s</string>" % g )
            output_body.append("\t\t\t</array>")

        output_body.append("\t\t\t<key>Path</key>\n\t\t\t<string>%s</string>" % path )

        if dont_index:
            output_body.append("\t\t\t<key>TargetIndices</key>\n\t\t\t<array/>")

        output_body.append("\t\t</dict>")

    #
    # Generate the "Definitions" section
    #
    def generate_definitions( self ):
        output_header = "\t<key>Definitions</key>"
        output_dict_open = "\t<dict>"
        output_dict_close = "\t</dict>"

        output_body = []
        for path in self.files_to_include:

            # group name
            group = []
            # obtain group name from directory
            dirs = os.path.dirname(path)
            group = dirs.split('/')

            group = group[self.group_start_index:]

            # get the extension
            filename = os.path.basename(path)
            name_extension= filename.split('.')
            extension = None
            if len(name_extension) == 2:
                extension = name_extension[1]

            self.append_definition( output_body, path, group, extension in self.ignore_extensions )

        self.output.append( output_header )
        self.output.append( output_dict_open )
        self.output.append( "\n".join( output_body ) )
        self.output.append( output_dict_close )

    #
    # Generates the "Nodes" section
    #
    def generate_nodes( self ):
        output_header = "\t<key>Nodes</key>"
        output_open = "\t<array>"
        output_close = "\t</array>"

        output_body = []
        for path in self.files_to_include:
            path = self.prefix + '/' + path
            output_body.append("\t\t<string>%s</string>" % path )

        self.output.append( output_header )
        self.output.append( output_open )
        self.output.append( "\n".join( output_body ) )
        self.output.append( output_close )

    #
    # Generates the plist. Send it to to stdout
    #
    def generate_xml( self ):
        self.output.append( _template_open_body % self.identifier )
        if append != "":
            self.output.append( "".join(open(append, "r").readlines()) )
        self.generate_definitions()
        self.generate_nodes()
        self.output.append( _template_close_body )

        print "\n".join( self.output )

    def generate( self ):
        self.scandirs( self.directory )
        self.generate_xml()

def help():
    print "%s v1.0 - An utility to generate Xcode 4 templates" % sys.argv[0]
    print "Usage:"
    print "\t-d directory (directory to parse)"
    print "\t-g directory_used_as_starting_group (if 1, then 'libs/cocos2d/Support/' -> ['cocos2d','Support'] ignoring 'libs')"
    print "\t-i identifier (Xcode4 template identifier)"
    print "\nExample:"
    print "\t%s -d cocos2d -g 0 -i cocos2dlib" % sys.argv[0]
    sys.exit(-1)

if __name__ == "__main__":
    if len( sys.argv ) == 1:
        help()

    directory = None
    group = None
    identifier = None
    prefix = ""
    exclude = []
    append = ""

    argv = sys.argv[1:]
    try:
        opts, args = getopt.getopt(argv, "d:g:i:p:e:a:", ["directory=","group=","identifier=","prefix=", "exclude=", "append="])
        for opt, arg in opts:
            if opt in ("-d","--directory"):
                directory = arg
            if opt in ("-g","--group"):
                group = arg
            if opt in ("-i","--identifier"):
                identifier = arg
            if opt in ("-p","--prefix"):
                prefix = arg
            if opt in ("-e","--exclude"):
                exclude = arg.split()
            if opt in ("-a","--append"):
                append = arg
    except getopt.GetoptError,e:
        print e

    if directory == None:
        help()

    #generate libs/cocos2dx
    gen = Xcode4Template( directory=directory, group=group, identifier=identifier, prefix=prefix, exclude=exclude, append=append )
    gen.generate()


code:
	<key>Project</key>
	<dict>
		<key>Configurations</key>
		<dict>
			<key>Debug</key>
			<dict>
				<key>GCC_PREPROCESSOR_DEFINITIONS</key>
				<array>
					<string>CC_LUA_ENGINE_ENABLED=1</string>
				</array>
			</dict>
			<key>Release</key>
			<dict>
				<key>GCC_PREPROCESSOR_DEFINITIONS</key>
				<array>
					<string>CC_LUA_ENGINE_ENABLED=1</string>
				</array>
			</dict>
		</dict>
	</dict>


code:
module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    "rules": {
    }
};

repo_name: VlaDDoS/porject

license: unknown


porject-yoga

license: ISC

acorn: bin/acorn

type: github

node-which: bin/node-which


test: echo "Error: no test specified" && exit 1

type: git


code:
.fade {
  animation-name: fade;
  animation-duration: 2.5s;
  -webkit-animation-name: fade;
  -webkit-animation-duration: 2.5s;
}
@-webkit-keyframes fade {
  from {
    opacity: 0.1;
  }
  to {
    opacity: 1;
  }
}
@keyframes fade {
  from {
    opacity: 0.1;
  }
  to {
    opacity: 1;
  }
}
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  outline: none;
  font-family: "Roboto Slab";
}
a {
  color: inherit;
  text-decoration: none;
}
a:visited,
a:hover,
a:focus,
a:active {
  text-decoration: none;
  color: inherit;
}
header {
  width: 100%;
  height: 60px;
  position: fixed;
  top: 0;
  background-color: rgba(0, 0, 0, 0.4);
  z-index: 10;
}
header ul {
  height: 60px;
  display: flex;
  list-style-type: none;
  justify-content: space-around;
  align-items: center;
  color: #fff;
}
header ul li:after {
  content: '';
  display: block;
  width: 100%;
  height: 1px;
  background-color: #c78030;
  margin-top: 4px;
  transition: 0.4s;
}
header ul li:before {
  content: '';
  display: block;
  width: 100%;
  right: 0;
  height: 1px;
  background-color: #c78030;
  margin-top: 4px;
  transition: 0.4s;
}
.main {
  height: 650px;
  width: 100%;
  background: url(../img/bg.jpg) center no-repeat;
  background-size: cover;
}
.main-block {
  width: 750px;
  min-height: 300px;
  border: 6px solid #c78030;
  background-color: rgba(0, 0, 0, 0.4);
  margin: 0 auto;
  padding: 40px;
  margin-top: 180px;
  text-align: center;
}
.main-block-link {
  color: #fff;
  font-size: 22px;
  font-weight: bold;
}
.main-block-link:after {
  content: '';
  display: block;
  width: 10%;
  height: 3px;
  background-color: #fff;
  margin: 0 auto;
  margin-top: 2px;
}
.main-block-title {
  color: #fff;
  text-transform: uppercase;
  font-size: 52px;
  margin-top: 35px;
}
.main-block-descr {
  color: #c78030;
  text-transform: uppercase;
  font-size: 22px;
  margin-top: 35px;
  font-weight: bold;
}
.content {
  width: 100%;
  min-height: 3200px;
  background: url(../img/main_bg.jpg);
  background-position: 50% 30%;
}
.content .info {
  width: 100%;
  min-height: 580px;
  background-color: #373737;
  padding: 50px;
  box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.8);
}
.content .info-header {
  width: 100%;
  height: 50px;
  display: flex;
  justify-content: space-around;
}
.content .info-header-tab {
  color: #c78030;
  font-weight: bold;
  font-size: 24px;
  cursor: pointer;
}
.content .info-header-tab:after {
  content: '';
  display: block;
  width: 0%;
  height: 3px;
  background-color: #c78030;
  margin-top: 4px;
  transition: 0.4s;
}
.content .info-header-tab:hover:after {
  width: 60%;
}
.content .info-tabcontent {
  width: 100%;
  display: flex;
  padding-top: 50px;
  transition: 0.3s;
}
.content .info-tabcontent .description,
.content .info-tabcontent .photo {
  width: 50%;
}
.content .info-tabcontent .description-title {
  font-size: 34px;
  color: #c78030;
}
.content .info-tabcontent .description-title:after {
  content: '';
  display: block;
  width: 20%;
  height: 3px;
  background-color: #c78030;
  margin-top: 4px;
}
.content .info-tabcontent .description-text {
  margin-top: 30px;
  color: #fff;
  font-weight: 300;
  padding-right: 20px;
}
.content .info-tabcontent .description-btn {
  width: 180px;
  height: 45px;
  line-height: 43px;
  margin-top: 30px;
  border: 1px solid #c78030;
  font-size: 14px;
  color: #c78030;
  text-transform: uppercase;
  font-weight: bold;
  text-align: center;
  font-weight: 300;
  cursor: pointer;
}
.content .info-tabcontent .description-btn:hover {
  box-shadow: 0px 0px 5px #c78030;
}
.content .info-tabcontent .photo {
  display: flex;
  align-items: center;
}
.content .info-tabcontent .photo img {
  width: 100%;
  box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.8);
}
.content .info .hide {
  display: none;
}
.content .info .show {
  display: flex;
}
.content .timer {
  margin-top: 150px;
  text-align: center;
}
.content .timer-title {
  font-size: 36px;
  color: #c78030;
}
.content .timer-action {
  font-size: 24px;
  color: #fff;
  margin-top: 40px;
}
.content .timer-numbers {
  margin-top: 40px;
  font-size: 90px;
  color: #c78030;
}
.content .timer-numbers:after {
  content: '';
  display: block;
  width: 20%;
  height: 1px;
  background-color: #c78030;
  margin: 0 auto;
  margin-top: 4px;
}
.content .slider {
  margin-top: 150px;
  position: relative;
}
.content .slider-title {
  font-size: 32px;
  color: #c78030;
}
.content .slider-title:after {
  content: '';
  display: block;
  width: 10%;
  height: 1px;
  background-color: #c78030;
  margin-top: 4px;
}
.content .slider .wrap {
  width: 100%;
  position: relative;
}
.content .slider .wrap .slider-item {
  width: 80%;
  margin: 0 auto;
  margin-top: 50px;
  position: relative;
}
.content .slider .wrap .slider-item img {
  width: 100%;
}
.content .slider .wrap .prev {
  position: absolute;
  cursor: pointer;
  top: 0;
  width: 90px;
  left: 10%;
  background-color: rgba(0, 0, 0, 0);
  transition: 0.3s;
  height: 100%;
}
.content .slider .wrap .prev:hover {
  background-color: rgba(0, 0, 0, 0.4);
}
.content .slider .wrap .prev:hover .arrow-left {
  opacity: 1;
}
.content .slider .wrap .prev .arrow-left {
  width: 30px;
  height: 30px;
  background: url(../icons/left-arrow.svg) center no-repeat;
  background-size: cover;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%);
  opacity: 0.4;
  transition: 1s;
}
.content .slider .wrap .next {
  position: absolute;
  cursor: pointer;
  top: 0;
  width: 90px;
  background-color: rgba(0, 0, 0, 0);
  transition: 0.3s;
  height: 100%;
  right: 10%;
}
.content .slider .wrap .next:hover {
  background-color: rgba(0, 0, 0, 0.4);
}
.content .slider .wrap .next:hover .arrow-right {
  opacity: 1;
}
.content .slider .wrap .next .arrow-right {
  width: 30px;
  height: 30px;
  background: url(../icons/right-arrow.svg) center no-repeat;
  background-size: cover;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%);
  opacity: 0.4;
  transition: 1s;
}
.content .slider-dots {
  width: 90%;
  margin: 0 auto;
  margin-top: 20px;
  display: flex;
  justify-content: center;
}
.content .slider-dots .dot {
  cursor: pointer;
  height: 16px;
  width: 16px;
  margin: 0 10px;
  background-color: #c78030;
  border-radius: 50%;
  display: inline-block;
  transition: background-color 0.6s ease;
}
.content .slider-dots .dot-active {
  background-color: #000;
  height: 18px;
  width: 18px;
}
.content .counter {
  margin-top: 150px;
  /* Для Firefox */
}
.content .counter-title {
  font-size: 32px;
  color: #c78030;
}
.content .counter-title:after {
  content: '';
  display: block;
  width: 20%;
  height: 1px;
  background-color: #c78030;
  margin-top: 4px;
}
.content .counter-block {
  margin-top: 40px;
  width: 45%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.content .counter-block-option {
  font-size: 22px;
  color: #c78030;
}
.content .counter-block-input {
  display: block;
  width: 120px;
  height: 60px;
  padding: 10px;
  color: #c78030;
  background-color: transparent;
  border: none;
  border-bottom: 1px solid rgba(199, 126, 48, 0.6);
  font-size: 46px;
  text-align: center;
}
.content .counter-block select {
  width: 230px;
  height: 35px;
  padding-left: 10px;
  background-color: rgba(199, 126, 48, 0.8);
  color: #fff;
  border: none;
}
.content .counter-total {
  margin-top: 30px;
  padding-right: 50px;
  text-align: right;
  font-size: 38px;
  color: #c78030;
}
.content .counter-total span {
  font-size: 46px;
}
.content .counter input[type=number]::-webkit-inner-spin-button,
.content .counter input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  margin: 0;
}
.content .counter input[type=number] {
  -moz-appearance: textfield;
}
.content .more {
  display: block;
  margin: 0 auto;
  margin-top: 60px;
  width: 250px;
  height: 60px;
  background-color: transparent;
  border: 1px solid #c78030;
  font-size: 18px;
  color: #c78030;
  cursor: pointer;
}
.content .more:hover {
  box-shadow: 0 0 10px #c78030;
}
.content .contact {
  width: 86%;
  height: 400px;
  display: flex;
  background-color: #373737;
  margin: 0 auto;
  margin-top: 150px;
  box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.8);
}
.content .contact-img {
  width: 50%;
}
.content .contact-img img {
  height: 550px;
  transform: translateY(-13%);
}
.content .contact-form {
  width: 50%;
  padding-left: 40px;
}
.content .contact-form-title {
  margin-top: 30px;
  color: #c78030;
  font-size: 26px;
  font-weight: 300;
}
.content .contact-form-title:after {
  content: '';
  display: block;
  width: 20%;
  height: 1px;
  background-color: #c78030;
  margin-top: 4px;
}
.content .contact-form form input {
  display: block;
  width: 60%;
  height: 40px;
  margin-top: 30px;
  background-color: transparent;
  border: none;
  border-bottom: 1px solid #fff;
  color: #fff;
}
.content .contact-form form button {
  margin-top: 60px;
  width: 150px;
  height: 40px;
  background-color: transparent;
  border: 1px solid #c78030;
  color: #c78030;
  cursor: pointer;
}
.content .contact-form form button:hover {
  box-shadow: 0 0 10px #c78030;
}
.content .contact-form form .status {
  margin-top: 20px;
  color: #fff;
}
.content footer {
  width: 100%;
  height: 250px;
  display: flex;
  align-items: center;
}
.content footer .social {
  display: flex;
  justify-content: center;
  align-items: center;
  padding-top: 20px;
}
.content footer .social .social-block {
  width: 70px;
  height: 70px;
  border: 1px solid rgba(199, 128, 48, 0.5);
  margin-right: 15px;
}
.content footer .social .social-block:hover {
  box-shadow: 0 0 10px #c78030;
}
.content footer .social .social-block a img {
  width: 50%;
  display: block;
  margin: 0 auto;
  margin-top: 17px;
}
.overlay {
  position: fixed;
  display: none;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 3;
}
.overlay .popup {
  position: fixed;
  z-index: 4;
  left: 50%;
  top: 150px;
  width: 752px;
  transform: translateX(-50%);
}
.overlay .popup-close {
  position: absolute;
  right: -20px;
  top: -35px;
  cursor: pointer;
  font-size: 35px;
  color: #fff;
  font-weight: 300;
}
.overlay .popup-title {
  display: block;
  width: 100%;
  height: 71px;
  line-height: 71px;
  margin: 0;
  background-color: #c78030;
  color: #ffffff;
  text-transform: uppercase;
  font-size: 21px;
  font-weight: 500;
  text-align: center;
}
.overlay .popup-form {
  background: url(../img/modal.jpg) center no-repeat;
  background-size: cover;
  padding: 35px 88px 50px 88px;
  color: #fff;
}
.overlay .popup-form-header {
  padding: 0;
  font-weight: 400;
  line-height: 27px;
  font-size: 30px;
  text-align: center;
}
.overlay .popup-form__label {
  display: block;
  margin-top: 23px;
  font-size: 20px;
  font-weight: 300;
  text-align: center;
}
.overlay .popup-form__input {
  width: 365px;
  height: 56px;
  margin-top: 17px;
  font-size: 18px;
  background-color: #c78030;
  border: none;
  color: #fff;
  text-align: center;
}
.overlay .popup-form__input::-webkit-input-placeholder {
  color: #fff;
}
.overlay .popup-form__input::-moz-placeholder {
  color: #fff;
}
.overlay .popup-form__input:-moz-placeholder {
  color: #fff;
}
.overlay .popup-form__input:-ms-input-placeholder {
  color: #fff;
}
.overlay .popup-form__input:focus::-webkit-input-placeholder {
  color: transparent;
}
.overlay .popup-form__input:focus::-moz-placeholder {
  color: transparent;
}
.overlay .popup-form__input:focus:-moz-placeholder {
  color: transparent;
}
.overlay .popup-form__input:focus:-ms-input-placeholder {
  color: transparent;
}
.overlay .popup-form__btn {
  width: 180px;
  height: 56px;
  font-size: 15px;
  line-height: 22px;
  margin-left: 16px;
  background-color: transparent;
  color: #c78030;
  border: 1px solid #c78030;
  cursor: pointer;
}
.overlay .popup-form__btn:hover {
  box-shadow: 0 0 10px #c78030;
}


code:
@main-color: #c78030;
@dark: #373737;

.fade {
  animation-name: fade;
  animation-duration: 2.5s;
  -webkit-animation-name: fade;
  -webkit-animation-duration: 2.5s;
}
@-webkit-keyframes fade {
  from {opacity:.1}
  to {opacity: 1}
}

@keyframes fade{
  from {opacity: .1}
  to {opacity: 1}
}

* {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
	outline: none;
	font-family: "Roboto Slab";
}

a {
	color:inherit;
	text-decoration: none;
	&:visited, &:hover, &:focus, &:active {
		text-decoration:none;
		color:inherit;
	}
}

header {
	width: 100%;
	height:60px;
	position: fixed;
	top:0;
	background-color: rgba(0,0,0,.4);
	z-index: 10;
	ul {
		height: 60px;
		display:flex;
		list-style-type: none;
		justify-content: space-around;
		align-items: center;
		color: #fff;
		li{
			&:after {
				content: '';
				display: block;
				width: 100%;
				height: 1px;
				background-color: @main-color;
				margin-top: 4px;
				transition:0.4s;
			}
			&:before {
				content: '';
				display: block;
				width: 100%;
				right: 0;
				height: 1px;
				background-color: @main-color;
				margin-top: 4px;
				transition:0.4s;
			}
		}
	}
}
.main {
	height: 650px;
	width: 100%;
	background: url(../img/bg.jpg) center no-repeat;
	background-size: cover;
	&-block {
		width: 750px;
		min-height: 300px;
		border: 6px solid @main-color;
		background-color: rgba(0,0,0,.4);
		margin: 0 auto;
		padding: 40px;
		margin-top: 180px;
		text-align: center;

		&-link {
			color: #fff;
			font-size: 22px;
			font-weight: bold;
			&:after {
				content: '';
				display: block;
				width: 10%;
				height: 3px;
				background-color: #fff;
				margin: 0 auto;
				margin-top: 2px;
			}
		}
		&-title {
			color: #fff;
			text-transform: uppercase;
			font-size: 52px;
			margin-top: 35px;

		}
		&-descr {
			color: @main-color;
			text-transform: uppercase;
			font-size: 22px;
			margin-top: 35px;
			font-weight: bold;
		}
	}
}
.content {
	width: 100%;
	min-height:3200px;
	background: url(../img/main_bg.jpg);
	background-position: 50% 30%;
	.info {
		width: 100%;
		min-height: 580px;
		background-color: @dark;
		padding: 50px;
		box-shadow: 0px 0px 30px rgba(0,0,0, .8);
		&-header {
			width: 100%;
			height: 50px;
			display: flex;
			justify-content: space-around;
			&-tab {
				color: @main-color;
				font-weight: bold;
				font-size: 24px;
				cursor: pointer;
				&:after {
					content: '';
					display: block;
					width: 0%;
					height: 3px;
					background-color: @main-color;
					margin-top: 4px;
					transition:0.4s;
				}
				&:hover {
					&:after {
						width: 60%;
					}
				}
			}
		}
		&-tabcontent {
			width: 100%;
			display: flex;
			padding-top: 50px;
			transition: 0.3s;
			.description, .photo {
				width: 50%;
			}
			.description{
				&-title {
					font-size: 34px;
					color: @main-color;
					&:after {
						content: '';
						display: block;
						width: 20%;
						height: 3px;
						background-color: @main-color;
						margin-top: 4px;
					}
				}
				&-text {
					margin-top: 30px;
					color: #fff;
					font-weight: 300;
					padding-right: 20px;
				}
				&-btn {
					width: 180px;
					height:45px;
					line-height: 43px;
					margin-top: 30px;
					border: 1px solid @main-color;
					font-size: 14px;
					color: @main-color;
					text-transform: uppercase;
					font-weight: bold;
					text-align: center;
					font-weight: 300;
					cursor: pointer;
					&:hover {
						box-shadow: 0px 0px 5px @main-color;
					}
				}
			}
			.photo {
				display: flex;
				align-items: center;
				img{
					width: 100%;
					box-shadow: 0px 0px 30px rgba(0,0,0, .8);
				}
			}
		}
		.hide {
			display:none;
		}
		.show {
			display: flex;
		}
	}
	.timer {
		margin-top: 150px;
		text-align: center;
		&-title {
			font-size: 36px;
			color: @main-color;
		}
		&-action {
			font-size: 24px;
			color: #fff;
			margin-top: 40px;
		}
		&-numbers {
			margin-top: 40px;
			font-size: 90px;
			color: @main-color;
			&:after {
				content: '';
				display: block;
				width: 20%;
				height: 1px;
				background-color: @main-color;
				margin: 0 auto;
				margin-top: 4px;
			}
		}
	}
	.slider {
		margin-top: 150px;
		position:relative;
		&-title {
			font-size: 32px;
			color: @main-color;
			&:after {
				content: '';
				display: block;
				width: 10%;
				height: 1px;
				background-color: @main-color;
				margin-top: 4px;
			}
		}
		.wrap{
			width: 100%;
			position:relative;
			.slider-item {
				width: 80%;
				margin: 0 auto;
				margin-top: 50px;
				position: relative;
				img{
					width: 100%;
				}
			}
			.prev{
				position:absolute;
				cursor: pointer;
				top: 0;
				width: 90px;
				left: 10%;
				background-color: rgba(0,0,0,.0);
				transition: 0.3s;
				height: 100%;
				&:hover {
					background-color: rgba(0,0,0,.4);
					.arrow-left{
						opacity:1;
					}
				}
				.arrow-left {
					width: 30px;
					height: 30px;
					background: url(../icons/left-arrow.svg) center no-repeat;
					background-size: cover;
					position: absolute;
					top: 50%;
					left: 50%;
					transform: translate(-50%);
					opacity: .4;
					transition: 1s;
				}
			}
			.next {
				position:absolute;
				cursor: pointer;
				top: 0;
				width: 90px;
				background-color: rgba(0,0,0,.0);
				transition: 0.3s;
				height: 100%;
				right: 10%;
				&:hover {
					background-color: rgba(0,0,0,.4);
					.arrow-right{
						opacity:1;
					}
				}
				.arrow-right {
					width: 30px;
					height: 30px;
					background: url(../icons/right-arrow.svg) center no-repeat;
					background-size: cover;
					position: absolute;
					top: 50%;
					left: 50%;
					transform: translate(-50%);
					opacity: .4;
					transition: 1s;
				}
			}
		}

		&-dots {
			width: 90%;
			margin: 0 auto;
			margin-top: 20px;
			display: flex;
			justify-content: center;
			.dot {
				cursor:pointer;
				height:16px;
				width:16px;
				margin: 0 10px;
				background-color: @main-color;
				border-radius: 50%;
				display: inline-block;
				transition: background-color 0.6s ease;
				&-active {
					background-color: #000;
					height:18px;
					width:18px;
				}
			}
		}
	}
	.counter {
		margin-top: 150px;
		&-title {
			font-size: 32px;
			color: @main-color;
			&:after {
				content: '';
				display: block;
				width: 20%;
				height: 1px;
				background-color: @main-color;
				margin-top: 4px;
			}
		}
		&-block {
			margin-top: 40px;
			width: 45%;
			display: flex;
			justify-content: space-between;
			align-items: center;
			&-option {
			font-size: 22px;
			color: @main-color;
			}
			&-input {
				display: block;
				width: 120px;
				height: 60px;
				padding: 10px;
				color: @main-color;
				background-color: transparent;
				border: none;
				border-bottom: 1px solid rgba(199,126,48,.6);
				font-size: 46px;
				text-align: center;

			}
			select {
				width: 230px;
				height: 35px;
				padding-left: 10px;
				background-color: rgba(199,126,48,.8);
				color: #fff;
				border: none;
			}
		}
		&-total {
			margin-top: 30px;
			padding-right: 50px;
			text-align: right;
			font-size: 38px;
			color: @main-color;
			span {
				font-size: 46px;
			}
		}
		input[type=number]::-webkit-inner-spin-button,
		input[type=number]::-webkit-outer-spin-button {
		    -webkit-appearance: none;
		    -moz-appearance: none;
		    appearance: none;
		    margin: 0;
		}
		/* Для Firefox */
		input[type=number]{
		  -moz-appearance: textfield;
		}

	}
	.more {
		display: block;
		margin: 0 auto;
		margin-top: 60px;
		width: 250px;
		height: 60px;
		background-color: transparent;
		border: 1px solid @main-color;
		font-size: 18px;
		color: @main-color;
		cursor: pointer;
		&:hover{
			box-shadow: 0 0 10px @main-color;
		}
	}
	.contact {
		width: 86%;
		height: 400px;
		display: flex;
		background-color: @dark;
		margin: 0 auto;
		margin-top: 150px;
		box-shadow: 0px 0px 30px rgba(0,0,0, .8);
		&-img {
			width: 50%;
			img {
				height: 550px;
				transform: translateY(-13%);
			}
		}
		&-form {
			width: 50%;
			padding-left: 40px;
			&-title {
				margin-top: 30px;
				color: @main-color;
				font-size: 26px;
				font-weight: 300;
				&:after {
					content: '';
					display: block;
					width: 20%;
					height: 1px;
					background-color: @main-color;
					margin-top: 4px;
				}
			}
			form {
				input {
					display: block;
					width: 60%;
					height: 40px;
					margin-top: 30px;
					background-color: transparent;
					border: none;
					border-bottom: 1px solid #fff;
					color: #fff;
				}
				button {
					margin-top: 60px;
					width: 150px;
					height: 40px;
					background-color: transparent;
					border: 1px solid @main-color;
					color: @main-color;
					cursor: pointer;
					&:hover{
						box-shadow: 0 0 10px @main-color;
					}
				}
				.status {
					margin-top: 20px;
					color: #fff;
				}
			}
		}
	}
	footer {
		width: 100%;
		height: 250px;
		display: flex;
		align-items: center;
		.social {
			display: flex;
			justify-content: center;
			align-items: center;
			padding-top: 20px;
			.social-block {
				width: 70px;
				height: 70px;
				border: 1px solid rgba(199, 128, 48, .5);
				margin-right: 15px;
				&:hover{
					box-shadow: 0 0 10px @main-color;
				}
				a {
					img {
						width: 50%;
						display: block;
						margin: 0 auto;
						margin-top: 17px;
					}
				}
			}
		}
	}
}
.overlay{
	position: fixed;
	display: none;
	top: 0;
	width: 100%;
	height: 100%;
	background-color: rgba(0,0,0,.5);
	z-index: 3;
	.popup {
		position: fixed;
		z-index: 4;
		left: 50%;
		top: 150px;
		width: 752px;
		transform: translateX(-50%);
		&-close {
			position: absolute;
			right: -20px;
			top: -35px;
			cursor: pointer;
			font-size: 35px;
			color: #fff;
			font-weight: 300;
		}
		&-title {
			display: block;
			width: 100%;
			height: 71px;
			line-height: 71px;
			margin: 0;
			background-color: @main-color;
			color: #ffffff;
			text-transform: uppercase;
			font-size: 21px;
			font-weight: 500;
			text-align: center;
		}
		&-form {
			background: url(../img/modal.jpg) center no-repeat;
			background-size: cover;
			padding: 35px 88px 50px 88px;
			color: #fff;
			&-header {
				padding: 0;
				font-weight: 400;
				line-height: 27px;
				font-size: 30px;
				text-align: center;
			}
			&__label {
				display: block;
				margin-top: 23px;
				font-size: 20px;
				font-weight: 300;
				text-align: center;
			}
			&__input {
				width: 365px;
				height: 56px;
				margin-top: 17px;
				font-size: 18px;
				background-color: @main-color;
				border: none;
				color: #fff;
				text-align: center;
				&::-webkit-input-placeholder {color:#fff;}
				&::-moz-placeholder {color:#fff;}
				&:-moz-placeholder {color:#fff;}
				&:-ms-input-placeholder {color:#fff;}
				&:focus::-webkit-input-placeholder { color: transparent; }
				&:focus::-moz-placeholder          { color: transparent; }
				&:focus:-moz-placeholder           { color: transparent; }
				&:focus:-ms-input-placeholder      { color: transparent; }
			}
			&__btn {
				width: 180px;
				height: 56px;
				font-size: 15px;
				line-height: 22px;
				margin-left: 16px;
				background-color: transparent;
				color: @main-color;
				border: 1px solid @main-color;
				cursor: pointer;
				&:hover{
					box-shadow: 0 0 10px @main-color;
				}
			}
		}
	}
}


code:
<!DOCTYPE html>

	<meta charset="UTF-8">
	<title>Yoga</title>

	<link href="https://fonts.googleapis.com/css?family=Roboto+Slab:300,400,700&subset=cyrillic-ext" rel="stylesheet">
	<link rel="stylesheet" href="css/bootstrap-grid.min.css">
	<link rel="stylesheet" href="css/style.css">

О нас

Фото

Стоимость

Контакты

							Travel.org

							йога-туры в индию

							океан / йога / путешествия / аюрведа

Лечение

Отдых

Природа

Йога

Здоровый позвоночник

Йога, массажи и плавание в море - помогут уставшей спине! Индийские йоги считали, что здоровье человека можно определить по тому, насколько здоров и гибок у него позвоночник.
 Интересно, что бы древние йоги сказали, глядя на современного человека, который уже со школьного возраста мучается болями в спине, работает подолгу в неудобных сидячих позах и не умеет расслабляться, имеет искривление, которое в итоге приведет к болезням других органов? Йоги сказали бы – займись собой и срочно!

							Узнать подробнее

Антистресс

Аюрведа и йога утверждают, что главным источником здоровья нашего организма является здоровый ум. Программа «Антистресс» сначала убирает последствия стресса на физическом уровне, потом помогает избавиться от негативных и навязчивых мыслей, затем повышает общий уровень энергии.
 Вы наконец вспомните ощущение "свободной головы", ощутите прилив физических сил и вспомните, что такое счастье.

							Узнать подробнее

Восстановление

Стрессы, жизнь в условиях города, плохая экология, загрязненные продукты и вода, напряженный ритм жизни - все это день ото дня отбирает у нас молодость и хорошее здоровье.
 Одним кремом для лица не решить проблему омоложения организма, когда тебе за 40. Согласны?

							Узнать подробнее

Йога и аюрведа

Несколько лет назад мы разработали специальные программы по йоге и аюрведе - и мы поняли, что они отлично работают - на опыте 530 наших туристов! В каждой из этих программ есть одна цель, на достижение которой будут направлены и асаны, и дыхательные практики, и медитации, и аюрведические процедуры, будут читаться лекции по этой теме.
Йога и аюрведа - два сильных война, которые сообща будут бороться с проблемами и болезнями.

							Узнать подробнее

Успей забронировать место со скидкой

					До конца акции осталось

					00
					:
					00
					:
					00

			<button class="more"> Узнать больше</button>

Фото с наших поездок

Рассчитайте стоимость вашего отдыха

Количество людей

					<input type="number" min="1" step="1" id="people" class="counter-block-input">

На сколько дней

					<input type="number" min="1" step="1" id="days" class="counter-block-input">

Выберете базу

					<select name="place" id="select">
						<option id="mumbai" value="1">Индия, Мумбай</option>
						<option id="kerala" value="1.5">Индия, Керала</option>
						<option id="varanasi" value="1.8">Индия, Варанаси</option>
					</select>

					Общая сумма

					0

						Мы с вами свяжемся

					<form id="form">
						<input required type="email" name="email" placeholder="Ваша почта">
						<input required type="tel" name="phone" placeholder="Ваш телефон">
						<button type="submit">Отправить</button>
					</form>

×

				Форма обратной связи

						<form action="#" class="main-form">

								Узнайте больше о своём отдыхе

								Введите ваш номер телефона:

								<input class="popup-form__input" name="phone" type="tel" required placeholder="+7(978) 973 33 45">
								<button class="button popup-form__btn">
									Оставить заявку!
								</button>
						</form>


code:
export default class Calculator {
  constructor(peopleEl, daysEl, centerEl, totalEl) {
    this.people = peopleEl;
    this.days = daysEl;
    this.center = centerEl;
    this.totalValue = totalEl;

    this.countPersons = 0;
    this.countDays = 0;
    this.total = 0;

    this.startPrice = 4000;
  }

  init() {
    this._handleInputChange(this.people);
    this._handleInputChange(this.days);
    this._handleSelectChange();
  }

  _resetTotal() {
    this.total = 0;
    this.totalValue.innerText = this.total;
  }

  _resetSelect(select) {
    select.options.selectedIndex = 0;
  }

  _calcTotal(days, people, price) {
    if (!(days && people && price)) {
      this._resetTotal();
    } else {
      this.total = (+days + +people) * price;
    }
  }

  _handleInputChange(inputEl) {
    inputEl.addEventListener('input', ev => {
      const currentEl = ev.target;

      if (this.people.value === '' && this.days.value === '') {
        this._resetTotal();
        return;
      }

      if (currentEl.id === 'people') {
        this.countPersons = +currentEl.value;
      }

      if (currentEl.id === 'days') {
        this.countDays = +currentEl.value;
      }

      this._calcTotal(this.countDays, this.countPersons, this.startPrice);
      this.totalValue.innerText = this.total;

      this._resetSelect(this.center);
    })
  }

  _handleSelectChange() {
    this.center.addEventListener('change', event => {
      if (this.people.value === '' || this.days.value === '') {
        this._resetTotal();
        return;
      }

      const _total = this.total;
      const selectedEl = event.target;
      const seletedIndex = event.target.selectedIndex;

      this.totalValue.innerText = _total * selectedEl.options[seletedIndex].value;
    })
  }
}


code:
export default class Form {
  constructor(formEl, inputEl) {
    this.form = formEl;
    this.input = inputEl;

    this.message = {
      loading: 'Загрузка...',
      success: 'Спасибо! Мы скоро свяжемся с Вами',
      failure: 'Что-то пошло не так...',
    }
  }

  init() {
    this.handelForm();
  }

  createElStatusMessage() {
    const messageEl = document.createElement('div');
    messageEl.classList.add('status');

    return messageEl;
  }

  handelForm() {
    this.form.addEventListener('submit', event => {
      event.preventDefault()

      const statusMessage = this.createElStatusMessage();
      this.form.append(statusMessage);

      const json = this.convertToJSON();

      this.sendToServer(json)
        .then(() => statusMessage.innerText = this.message.loading)
        .then(() => statusMessage.innerText = this.message.success)
        .catch(() => statusMessage.innerText = this.message.failure)
        .finally(() => this.input.forEach(el => el.value = ''))
    });
  }

  convertToJSON() {
    const formData = new FormData(this.form);

    return JSON.stringify(Object.fromEntries(formData));
  }

  sendToServer(data) {

    return new Promise((res, rej) => {
      const request = new XMLHttpRequest();

      request.open('POST', 'server.php');
      request.setRequestHeader('Content-Type', 'application/json; charset=utf-8');

      request.addEventListener('readystatechange', () => {
        if (!request.readyState) {
          rej();
        }

        if (request.readyState === 4 && request.status === 200) {
          res();
        }

        if (request.readyState < 4) {
          res();
        }
      })

      request.send(data);
    })
  }
}


code:
export default class Modal {
  constructor(moreEl, descrBtnMoreEl, overlayEl, closeEl) {
    this.more = moreEl;
    this.descrBtnMore = descrBtnMoreEl;
    this.overlay = overlayEl;
    this.close = closeEl;
  }

  init() {
    // Tabs
    this.descrBtnMore.forEach((el) => {
      this.showModal(el);
      this.hiddenModal(el);
    });

    this.showModal(this.more);
    this.hiddenModal(this.more);
  }

  showModal(el) {
    el.addEventListener('click', () => {
      this.overlay.style.display = 'block';
      el.classList.add('more-splash');
      document.body.style.overflow = 'hidden';
    });
  }

  hiddenModal(el) {
    this.close.addEventListener('click', () => {
      this.overlay.style.display = 'none';
      el.classList.remove('more-splash');
      document.body.style.overflow = '';
    });
  }
}


code:
export default class Slider {
  constructor(slides, prev, next, dotsWrap, dots) {
    this.slideIndex = 1;

    this.slides = slides;
    this.prev = prev;
    this.next = next;
    this.dotsWrap = dotsWrap;
    this.dots = dots;
  }

  init() {
    this.showSlides(this.slideIndex);
    this.nextSlide();
    this.prevSlide();
    this.handleDots();
  }

  showSlides(n) {
    if (n > this.slides.length) {
      this.slideIndex = 1;
    }

    if (n < 1) {
      this.slideIndex = this.slides.length;
    }

    this.slides.forEach(el => el.style.display = 'none');
    this.dots.forEach(el => el.classList.remove('dot-active'));

    this.slides[this.slideIndex - 1].style.display = 'block';
    this.dots[this.slideIndex - 1].classList.add('dot-active');
  }

  nextSlide() {
    this.next.addEventListener('click', () => this.showSlides(this.slideIndex += 1));
  }

  prevSlide() {
    this.prev.addEventListener('click', () => this.showSlides(this.slideIndex -= 1));
  }

  handleDots() {
    this.dotsWrap.addEventListener('click', event => {
      for (let i = 0; i < this.dots.length + 1; i++) {
        if (event.target.classList.contains('dot') && event.target == this.dots[i - 1]) {
          this.showSlides(this.slideIndex = i);
        }
      }
    })
  }
}


code:
export default class Tabs {
  constructor(infoEl, tabsEl, tabContentEl) {
    this.info = infoEl;
    this.tabs = tabsEl;
    this.tabContent = tabContentEl;
  }

  init() {
    this.hideTabContent();
    this.handleClickTab();
  }

  /**
   * Метод скрывающий весь контент блока info кроме указанного в параметре метода
   * @param {number} a пробрасываем кол-во контента для показа
   */
  hideTabContent(a = 1) {
    for (let i = a; i < this.tabContent.length; i++) {
      this.tabContent[i].classList.remove('show');
      this.tabContent[i].classList.add('hide');
    }
  }

  /**
   * Метод показывающий определённый контент блока info
   * @param {number} b пробрасываем номер контента по которому произошёл клик
   */
  showTabContent(b) {
    if (this.tabContent[b].classList.contains('hide')) {
      this.tabContent[b].classList.remove('hide');
      this.tabContent[b].classList.add('show');
    }
  }

  handleClickTab() {
    this.info.addEventListener('click', event => {
      const target = event.target;

      if (target && target.classList.contains('info-header-tab')) {
        for (let i = 0; i < this.tabs.length; i++) {
          if (target === this.tabs[i]) {
            this.hideTabContent(0);
            this.showTabContent(i);
            return;
          }
        }
      }
    });
  }
}


code:
export default class Timer {
  constructor(timerEl, hoursEl, minutesEl, secondsEl) {
    this.deadLine = '2021-12-27';

    this.timer = timerEl;
    this.hours = hoursEl;
    this.minutes = minutesEl;
    this.seconds = secondsEl;
  }

  init() {
    this.setClock();
  }

  /**
   * Получем оставшиеся время
   * @param {string} endTime
   * @returns Объект значений
   */
  getRemaingTime() {
    const currentTime = new Date(),
      total = Date.parse(this.deadLine) - Date.parse(currentTime),
      seconds = Math.floor((total / 1000) % 60),
      minutes = Math.floor((total / 1000 / 60) % 60),
      hours = Math.floor(total / (1000 * 60 * 60));

    return {
      total: total,
      hours: hours,
      minutes: minutes,
      seconds: seconds,
    };
  }

  /**
   * Получает элементы со страницы
   */
  setClock() {
    const updateTimer = () => {
      const t = this.getRemaingTime();

      this.hours.innerText = t.hours < 10
        ? `0${t.hours}`
        : t.hours;

      this.minutes.innerText = t.minutes < 10
        ? `0${t.minutes}`
        : t.minutes;

      this.seconds.innerText = t.seconds < 10
        ? `0${t.seconds}`
        : t.seconds;

      if (t.total <= 0) {
        clearInterval(timeInterval);

        this.hours.innerText = '00';
        this.minutes.innerText = '00';
        this.seconds.innerText = '00';
      }
    };

    const timeInterval = setInterval(updateTimer, 1000);
  }
}


code:
import Tabs from './components/Tabs.js';
import Timer from './components/Timer.js';
import Modal from './components/Modal.js';
import Form from './components/Form.js';
import Slider from './components/Slider.js';
import Calculator from './components/Calculator.js';

window.addEventListener('DOMContentLoaded', () => {

  // Tabs

  const info = document.querySelector('.info-header'),
    tabs = document.querySelectorAll('.info-header-tab'),
    tabContent = document.querySelectorAll('.info-tabcontent');

  const tabModule = new Tabs(info, tabs, tabContent);
  tabModule.init();

  // Timer

  const timer = document.querySelector('#timer'),
    hours = timer.querySelector('.hours'),
    minutes = timer.querySelector('.minutes'),
    seconds = timer.querySelector('.seconds');

  const timerModule = new Timer(timer, hours, minutes, seconds);
  timerModule.init();

  // Modal

  const more = document.querySelector('.more'),
    descrBtnMore = document.querySelectorAll('.description-btn'),
    overlay = document.querySelector('.overlay'),
    close = document.querySelector('.popup-close');

  const modalModule = new Modal(more, descrBtnMore, overlay, close);
  modalModule.init();

  // FromModal

  const formModal = document.querySelector('.main-form'),
    inputModal = formModal.querySelectorAll('input');

  const formModuleModal = new Form(formModal, inputModal);
  formModuleModal.init();

  // Form

  const formEl = document.querySelector('#form'),
    inputFormEl = formEl.querySelectorAll('input');

  const formModule = new Form(formEl, inputFormEl);
  formModule.init();

  // Slider

  const slides = document.querySelectorAll('.slider-item'),
    prev = document.querySelector('.prev'),
    next = document.querySelector('.next'),
    dotsWrap = document.querySelector('.slider-dots'),
    dots = document.querySelectorAll('.dot');

  const sliderModule = new Slider(slides, prev, next, dotsWrap, dots);
  sliderModule.init();

  // Calculator

  const countPeople = document.querySelector('#people'),
    countDays = document.querySelector('#days'),
    center = document.querySelector('#select'),
    totalValue = document.querySelector('#total');

  const calculatorModule = new Calculator(countPeople, countDays, center, totalValue);
  calculatorModule.init();
})


code:
<?php

echo $_SERVER[‘REMOTE_ADDR‘];


code:
/node_modules/
/package-lock.json
/.lumo_cache/
/target/
data*/
var*/
token-*
/img/
_production

repo_name: panselia/jumpluff

path: .gitignore

license: wtfpl


code:
export JUMPLUFF_DATA ?= data
export JUMPLUFF_CONFIG ?= var/config.json
export DISCORD_TOKEN ?=
export JUMPLUFF_INLINE_DELIMITER ?= ,
export JUMPLUFF_QUERY_PREFIX ?=
export JUMPLUFF_HOSTED_BY ?=
export OSM_TILE_SERVER ?=

LUMO_PORT ?= 5555

deps:
	lein deps
	npm install \
		"lumo-cljs@1.9.0" \
		"discord.js@^11.4.2" \
		"csv@^4.0.0" \
		"fuse.js@^3.3.0"

run:
	./node_modules/.bin/lumo -K -D andare:0.7.0 -c src -m jumpluff.core

run-with-socket:
	./node_modules/.bin/lumo -n "${LUMO_PORT}" -K -D andare:0.7.0 -c src -m jumpluff.core

path: Makefile


code:
# Jumpluff - Pokémon Go Discord Bot

Query spreadsheets of points of interest to provide easy [Google Maps](https://maps.google.com/) pinning of Pokémon Go gyms

## Dependencies
- [Node.js](https://nodejs.org/en/) with npm
  - [lumo: Fast, cross-platform, standalone ClojureScript environment](https://github.com/anmonteiro/lumo)
  - [Fuse.js - JavaScript fuzzy-search library](http://fusejs.io/)
  - [discord.js](https://discord.js.org/)
  - [csv - npm](https://www.npmjs.com/package/csv)
- [Leiningen](https://leiningen.org/) to install [andare: core.async for bootstrapped ClojureScript](https://github.com/mfikes/andare)

## Setup

### Install dependencies
```shell
make deps
```

### Register a bot with Discord
<https://discordapp.com/developers/applications/me>
Copy the token

### Invite the bot to your server by generating and visiting an oauth link
<https://discordapp.com/developers/applications/me>
Select the following scopes:
- bot

And the following permissions:
- Manage Messages - to clear reactions on responses

### Run an instance
```shell
make DISCORD_TOKEN=<PASTE_TOKEN_HERE> run
```

#### Other run-time variables
- `JUMPLUFF_DATA` Directory of datasets
- `JUMPLUFF_VAR` Directory for the config file
- `JUMPLUFF_INLINE_DELIMITER` Character to use for inline queries
- `JUMPLUFF_QUERY_PREFIX` Space delimited list of command prefixes to trigger a query

### Start talking to the instance

As a Discord server administrator, configure the bot with commands via mentions
`@jumpluff help admin`

### Register your datasets with the instance

`@jumpluff add-dataset`
Arguments:
1. Name used to refer to the dataset
2. Filename of a CSV file in `JUMPLUFF_DATA`
- Zero based indexes of CSV columns
  1. Name column
  2. LatLon column
  3. Location description column
  4. EX raid potential column
  4. Nickname column

### Assign a channel a default dataset to use in queries from there

`@jumpluff channel-dataset [dataset name]`

## Usage

A traditional bot command prefix from `JUMPLUFF_QUERY_PREFIX` or wrapped with double `JUMPLUFF_INLINE_DELIMITER` in a channel with a default dataset

Query language:
dataset name : name filter - description filter (-ex|+ex|+ex-t)

## Datasets

A comma-separated values file in `JUMPLUFF_DATA`
Containing a header line then the following columns in any order:
- Exact Gym Name
- Location Coordinates
As parsed by `(-?\d{1,2}\.\d*,-?\d{1,2}\.\d*)`
- Location Description
Cross streets, property address, location notes
As an intentional way to handle line breaks while maintaining compatibility with various CSV implementations, when loaded a full-width slash(／) is replaced with a line break.
- EX-Raid Eligible Status
EX for eligible gyms
EX-T for triggered ex raid gyms
- Nickname for the gym

### Pull from a Google Docs spreadsheet
Export to CSV <https://docs.google.com/spreadsheets/d/DOCID/export?format=csv>

<!-- As a future feature this exporting process may be possible with a fetch command -->
### Import/Export with org-mode
For local editing in Emacs, Org Mode has `org-table-export` and `org-table-import`

### Import from an old map service
Apply <https://www.spotamon.com/gymscript.js> as a userscript
Navigate to <https://www.pokemongomap.info/>
Use the new S2Grid button to export gyms to csv

## Legal
Pokémon And All Respective Names are Trademark & © of Nintendo 1996-2018


code:
(defproject jumpluff "0.0.0-SNAPSHOT"
  :dependencies [[andare "0.7.0"]])


code:
(ns jumpluff.client
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.commands :refer [commands commands-via-mention]]
            [jumpluff.state :as state]
            [jumpluff.poi-message :as poi-message]
            [jumpluff.utils :refer [promise-to-chan log]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def discord (js/require "discord.js"))

(defn- once-sent
  "Start the reaction collector on sent responses to `command-name`"
  [sent-message src-message command-name]
  (when poi-message/*enable-reactions*
    (when (some #{command-name} ["poi" :inline])
      (poi-message/poi-once-sent sent-message src-message))))

(defn- maybe-respond
  "Respond to `message` when `command-result` is a string or richembed object.
  When `command-result` is an async/chan recur with the taken value.
  On error when sending the message, retry once if it was a dns lookup error"
  [message cb command-result]
  (cond (= (type command-result)
           cljs.core.async.impl.channels/ManyToManyChannel)
        (async-macros/go
          (maybe-respond message cb (async/<! command-result)))
        (not (or (boolean? command-result) ; handle embeds or strings
                 (nil? command-result)))
        (async-macros/go
          (when-let [sent-message (->> message.channel
                                     (promise-to-chan #(.send % command-result))
                                     async/<!)]
            (cb sent-message)))))

(defn- guild-admin?
  "When `message`.member is non-nil, check for admin permission.
  message.member will be nil when the message originated from a DM"
  [message]
  (some-> message.member
          (.hasPermission "ADMINISTRATOR")))

(defn- cmd
  "Invoke and respond to COMMAND-NAME if the permission is correct via MESSAGE."
  [message command-name argument-str]
  (when-let [command (get commands command-name)]
    (when (cond (= :admin (:privilege command))
                (guild-admin? message)
                (= :open (:privilege command))
                true)
      (let [argument (s/trimr (or argument-str ""))]
        (if-not (= command-name :inline)
          (log "—Invoking '" command-name "' '" argument "'"))
        ((:action command) message argument)))))

(defn- on-message
  [client]
  (fn [message]
    (if (not-any? nil? [message message.content])
      (do
        (log (if (nil? message.guild)
               message.channel.type
               (str message.guild.nameAcronym "#" message.channel.name))
             "|" message.author.tag
             (when (guild-admin? message)
               "(admin)") ": "
             (or (not-empty message.content)
                 (str "(embeds:" (count message.embeds) ")")))
        (when (and (not= message.author.id client.user.id)
                   (not message.author.bot))
          (let [mentioned? (.isMentioned message client.user)
                content (if mentioned?
                          (-> message.cleanContent
                             (s/replace (str "@" client.user.username) "")
                             (s/replace #"\s{2,}" " "))
                          message.content)
                [first-word rest-str] (-> content
                                         s/triml
                                         (s/split #" " 2))
                [command-name command-argument]
                (if mentioned?
                  (let [first-word-lowercase (s/lower-case first-word)]
                    (when (some #{first-word-lowercase} commands-via-mention)
                      [first-word-lowercase rest-str]))
                  (if (some #{first-word} state/poi-query-prefixes)
                    ["poi" rest-str]
                    [:inline]))]
            (when (not (nil? command-name))
              (some->> (cmd message command-name command-argument)
                       (maybe-respond message
                                      #(once-sent % message command-name)))))))
      (console.error message))))

(defn client-start
  [token]
  (let [client (discord.Client.)]
    (doto client
      (.on "ready" #(log "Serving " (count (.array client.guilds)) " server(s) as " client.user.tag))
      (.on "message" (on-message client))
      (.on "disconnected" #((log "disconnected, attempting to re-log in")
                            (.login client token)))
      (.on "error" console.error)
      (.login token))))

(defn client-finish
  [client]
  (async-macros/go
    (async/<! (poi-message/cleanup-msg-map))
    (async/<! (promise-to-chan (.destroy client)))))


code:
(ns jumpluff.commands
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.state :as state]
            [jumpluff.poi-message :as poi-message]
            [jumpluff.config :refer [load-config write-config]]
            [jumpluff.datasets :refer [load-dataset]]
            [jumpluff.poi :refer [poi-search]]
            [jumpluff.utils :refer [log
                                    dataset-names
                                    dataset-names-some-exact-match
                                    server-emoji-id]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def discord (js/require "discord.js"))

(defn log-poi-coll [coll log-msg]
  (log log-msg (take poi-message/*page-size* coll))
  coll)

(defn channel-id [channel]
  (str (if (not (nil? channel.guild))
         (.-id channel.guild)
         channel.type)
       "-" (or (.-parentID channel) "")
       "#" (or (.-name channel) channel.type)))

(defn dataset-name-for-channel
  "Get the default dataset name for `channel`, when unset return nil or `else`"
  ([channel]
   (get @state/channel-dataset (channel-id channel)))
  ([channel else]
   (get @state/channel-dataset (channel-id channel) else)))

(defn add-dataset
  ""
  [input-string]
  (let [ret (async/chan)
        args (remove empty? (s/split (s/trim input-string) " "))
        [name source] (take 2 args)
        [index-name index-gps index-description index-ex index-nicknames]
        (map #(let [n (js/parseInt %1)]
                (if (int? n) n nil))
             (drop 2 args))]
    (cond
      (not (not-any? nil? [name source index-name index-gps]))
      (async/put! ret (str "Incorrect params."
                           "\nExpected [name] [source] [index-name] [index-gps] [index-description?] [index-ex?] [index-nicknames?]"))
      (not (re-find #"^[\w-]*.csv$" source))
      (async/put! ret "Source as a filename isn't a plain csv filename")
      (not (apply distinct?
                  (remove nil? [index-name index-gps
                                index-description index-ex index-nicknames])))
      (async/put! ret "Indexes are not distinct")
      :else (do (swap! state/dataset-descriptions assoc name
                       {:source source
                        :columns {:index-name index-name
                                  :index-gps index-gps
                                  :index-description index-description
                                  :index-ex index-ex
                                  :index-nicknames index-nicknames}})
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret (str "Added and saved: " (get @state/dataset-descriptions name)))))))
    ret))

(defn cmd-add-dataset
  ""
  [_message input-string]
  (add-dataset input-string))

(def inline-delimiter
  (or (not-empty js/process.env.JUMPLUFF_INLINE_DELIMITER)
      ","))
(def inline-collecting-regex--regex
  ;; #"(?s)\,{2}\s{0,3}([^,]{2,128})\s{0,3}\,{2}(.*)$"
  (-> "(?s)\\X{2}\\s{0,3}([^X]{2,128})\\s{0,3}\\X{2}(.*)$"
     (s/replace "X" inline-delimiter)
     re-pattern))
(defn inline-collecting-regex
  "Lazily collect each substring contained between double `inline-delimiter` in INPUT-STRING"
  [input-string & collected]
  (lazy-seq
   (if (empty? input-string)
     (reverse (remove nil? collected))
     (let [[full match rest] (re-find inline-collecting-regex--regex input-string)]
       (apply inline-collecting-regex rest match collected)))))

(defn cmd-inline
  "Default command run on each message.
  First check if the message.channel has a dataset-name registered.
  Then use `inline-collecting-regex` to collect up to 5 inline queries for `poi-search`"
  [message argument]
  (when-let [dataset-name (dataset-name-for-channel message.channel)]
    (when-let [queries (not-empty (take poi-message/*page-size* (inline-collecting-regex message.content)))]
      (log "—found inline queries: " (s/join ", " queries))
      (-> (flatten (map #(poi-search dataset-name %1) queries))
         (log-poi-coll "cmd-inline results: ")
         (poi-message/poi-collection-and-src-msg-to-obj message)
         poi-message/poi-collection-to-message))))

(defn cmd-channel-dataset
  [message argument]
  (let [ret (async/chan)]
    (cond (empty? argument)
          (async/put! ret
                      (dataset-name-for-channel message.channel
                                                "Channel does not have a default dataset"))
          (dataset-names-some-exact-match argument)
          (do (swap! state/channel-dataset
                     assoc (channel-id message.channel)
                     argument)
              (async-macros/go
                (if (async/<! (write-config))
                  (async/put! ret "error writing config")
                  (async/put! ret "Set and saved."))))
          :else
          (async/put! ret (str "Invalid dataset name."
                               "\nValid options are: "
                               (s/join ", " (dataset-names)))))
    ret))

(defn cmd-poi-query
  [message argument]
  (or (some-> (-> (dataset-name-for-channel message.channel)
                 (poi-search argument))
              (log-poi-coll "cmd-poi-query results: ")
              (poi-message/poi-collection-and-src-msg-to-obj message)
              poi-message/poi-collection-to-message)
      "Channel does not have a default dataset to act on and one was not provided in the query."))

(defn load-config-then-datasets
  "Use `data-dir` and `config-file` to fill `state/config` and `state/csv-files`"
  ([]
   (load-config-then-datasets state/data-dir state/config-file))
  ([data-dir config-file]
   (async-macros/go
     (async/<! (load-config config-file))
     (doseq [pair @state/dataset-descriptions]
       (async/<! (load-dataset state/csv-files data-dir pair))))))
(defn cmd-reload
  [message argument]
  (let [ret (async/chan)]
    (async-macros/go
      (async/<! (load-config-then-datasets))
      (async/put! ret "Reloaded config and datasets"))
    ret))

(defn cmd-set-emoji
  "register a discord(possibly server local) emoji for the bot to use
  to type a custom emoji in discord, there is an autocomplete after typing a colon"
  [message argument]
  (let [ret (async/chan)]
    (let [[name emoji] (s/split argument #" " 2)]
      (cond (and (= name "ex")
                 (not-empty emoji))
            (do (swap! state/server-emoji
                       assoc (server-emoji-id message name)
                       emoji)
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else
            (async/put! ret "Invalid arguments given. Expected 'ex :ex:'")))
    ret))

(defn set-dataset-note
  "attatch a message to a dataset id"
  [input-string]
  (let [ret (async/chan)]
    (let [[dataset-name note] (s/split input-string #" " 2)]
      (cond (dataset-names-some-exact-match dataset-name)
            (do (when-let [ds (get @state/dataset-descriptions dataset-name)]
                  (swap! state/dataset-descriptions assoc dataset-name
                         (assoc ds :note note)))
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else (async/put! ret "Given dataset name does not already exist.")))
    ret))
(defn cmd-set-dataset-note
  "attatch a message to a dataset id"
  [_message argument]
  (set-dataset-note argument))
(defn cmd-dataset-note
  [message argument]
  (if-let [dataset-name (if (not-empty argument)
                          (dataset-names-some-exact-match argument)
                          (dataset-name-for-channel message.channel))]
    (or (some->> dataset-name
                 (get @state/dataset-descriptions)
                 :note)
        "No saved note for this dataset")
    "Invalid or no default or no given dataset name to lookup"))

(defn add-dataset-abbrv
  "Add an abbreviation to expand before poi-filter-by-name for a dataset-name"
  [input-string]
  (let [ret (async/chan)]
    (let [[dataset-name abbrv expansion] (map s/trim (s/split input-string #" " 3))]
      (cond (dataset-names-some-exact-match dataset-name)
            (do (when-let [ds (get @state/dataset-descriptions dataset-name)]
                  (let [abbrvs (get ds :abbrv {})]
                    (swap! state/dataset-descriptions assoc dataset-name
                           (assoc ds :abbrv
                                  (assoc abbrvs abbrv expansion)))))
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else (async/put! ret "Given dataset name does not already exist.")))
    ret))
(defn cmd-add-dataset-abbrv
  [_message argument]
  (add-dataset-abbrv argument))
(defn cmd-dataset-abbrv
  [message argument]
  (if-let [dataset-name (if (not-empty argument)
                          (dataset-names-some-exact-match argument)
                          (dataset-name-for-channel message.channel))]
    (str (or (some->> dataset-name
                      (get @state/dataset-descriptions)
                      :abbrv)
             "No saved note for this dataset"))
    "Invalid or no default or no given dataset name to lookup"))

(defn cmd-help
  [message argument]
  (let [base (doto (discord.RichEmbed.)
               (.setTitle (str "Jumpluff(@" (.-user.tag @state/client) ") - Point of Interest Bot"))
               (.setDescription (str "Query spreadsheets of gym names/locations/ex eligible status"
                                     " - [source code](https://notabug.org/panselia/jumpluff)\n"))
               (.setFooter (if (not-empty js/process.env.JUMPLUFF_HOSTED_BY)
                             (str "Instance hosted by " js/process.env.JUMPLUFF_HOSTED_BY)
                             ""))
               (.addField "Setup and usage"
                          (str "Raid channels can be assigned a default dataset with `channel-dataset`.\n"
                               "In those channels you can query the dataset inline by wrapping the query with `"
                               inline-delimiter inline-delimiter "`"))
               (.addField "User commands"
                          (str (s/join ", " (map #(str "`" % "`") state/poi-query-prefixes)) " - Perform a query\n"
                               "`@" (.-user.username @state/client) " help [verbose?]` - this usage message\n"
                               "`@" (.-user.username @state/client) " dataset-note [dataset-name?]` - Print the dataset's note\n"
                               "`@" (.-user.username @state/client) " dataset-abbrv [dataset-name?]` - View PoI name expansions for the dataset\n")))]
    (if (empty? argument)
      base
      (doto base
        (.addField "Server admin commands via mention"
                   (str "`add-dataset` - Add a dataset, refer to the README for arguments\n"
                        "`channel-dataset [dataset-name?]` - Set a default dataset for the channel, or show the current default\n"
                        "`set-emoji ex :ex:` - Register a possibly server local emoji to use to show on EX gyms\n"
                        "`set-dataset-note [dataset-name] [note]` - Attach a note to a dataset name\n"
                        "`add-dataset-abbrv [dataset-name] [abbrv] [expansion]` - Add an abbreviation that expands before poi-filter-by-name matching\n"
                        "`reload` - reload the config and datasets"))
        (.addField "Number of servers this instance is in"
                   (str (count (.array (.-guilds @state/client)))))
        (.addField "Bot uptime" (str (.-uptime @state/client) "ms"))
        (.addField "Loaded dataset names"
                   (s/join ", " (dataset-names)))
        (.addField "EX eligible server emoji"
                   (get @state/server-emoji
                        (server-emoji-id message "ex")
                        " (EX)"))))))

(def commands
  {"add-dataset" {:privilege :admin :action cmd-add-dataset}
   "channel-dataset" {:privilege :admin :action cmd-channel-dataset}
   "set-emoji" {:privilege :admin :action cmd-set-emoji}
   "reload" {:privilege :admin :action cmd-reload}
   "set-dataset-note" {:privilege :admin :action cmd-set-dataset-note}
   "dataset-note" {:privilege :open :action cmd-dataset-note}
   "add-dataset-abbrv" {:privilege :admin :action cmd-add-dataset-abbrv}
   "dataset-abbrv" {:privilege :open :action cmd-dataset-abbrv}
   "help" {:privilege :open :action cmd-help}
   "poi" {:privilege :open :action cmd-poi-query}
   :inline {:privilege :open :action cmd-inline}})

(def commands-via-mention (remove #{:inline} (keys commands))) ; cache it


code:
(ns jumpluff.config
  (:require [cljs.core.async :as async]
            [clojure.walk :as walk]
            [jumpluff.state :as state])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def fs (js/require "fs"))

(defn- write-map-to-json-file
  [obj filename]
  (let [err-chan (async/chan)]
    (.writeFile fs filename
                (.stringify js/JSON (clj->js obj))
                #(async/put! err-chan (or %1 false)))
    err-chan))

(defn write-config
  ;; "deref the atom values of the `conf` map, write to `config-file`"
  ([]
   (write-config state/config-file))
  ([config-file]
   (write-map-to-json-file
    {:channel-dataset @state/channel-dataset
     :dataset-descriptions @state/dataset-descriptions
     :server-emoji @state/server-emoji}
    config-file)))

(defn- load-json-file [filename]
  (let [json-chan (async/chan)]
    (.readFile fs filename
               #(async/put! json-chan
                            (js->clj (.parse js/JSON (or %2 "{}")))))
    json-chan))

(defn load-config
  ;; "Load `config-file` into the atom slots in `config`"
  [config-file]
  (async-macros/go
    (let [static-config (->> config-file
                           load-json-file
                           async/<!
                           (map (fn [[k v]] {(keyword k) v}))
                           (apply merge))]
      (reset! state/server-emoji (:server-emoji static-config))
      (reset! state/channel-dataset (:channel-dataset static-config))
      (reset! state/dataset-descriptions
              (->> (:dataset-descriptions static-config)
                 (map (fn [[k v]] {k (walk/keywordize-keys v)}))
                 (apply merge))))))


code:
(ns jumpluff.core
  (:require [cljs.core.async :as async]
            [jumpluff.state :as state]
            [jumpluff.commands :refer [load-config-then-datasets]]
            [jumpluff.client :refer [client-start client-finish]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(defn -main []
  (async-macros/go
    (async/<! (load-config-then-datasets))
    (->> js/process.env.DISCORD_TOKEN
       client-start
       (reset! state/client))))

;; Give it 5 seconds to clean up
(let [exit-handler (fn []
                     (js/setTimeout #(.exit js/process 1) 5000)
                     (async-macros/go
                       (async/<! (client-finish @state/client))
                       (.exit js/process 0)))]
  (.on js/process "SIGTERM" exit-handler)
  (.on js/process "SIGINT" exit-handler))


code:
(ns jumpluff.datasets)

(defmacro update-vec-from-index-map
  "Expand to a cond->, int?, assoc, and let nth"
  [xs index-map binding & {:as body-pairs}]
  `(cond-> ~xs
     ~@(->> body-pairs
          (map (fn [[k v]]
                 (list `(int? (~k ~index-map))
                       `(assoc (~k ~index-map)
                               (let [~binding (nth ~xs (~k ~index-map))]
                                 ~v)))))
          (apply concat))))

;; To define macros at the REPL: append $macros to the namespace
;; but invoke the macro with the public namespace
;; (ns jumpluff.datasets$macros)

;; (let [xs ["val1" "val2" "val3" "val4"]]
;;   (= (jumpluff.datasets/update-vec-from-index-map
;;       xs {:v1 0 :v2 1} <>
;;       :v1 (reverse <>)
;;       :v2 (reverse <>))
;;      (cond-> xs
;;        (int? 1)
;;        (assoc 1 (let [<> (nth xs 1)]
;;                   (reverse <>)))
;;        (int? 0)
;;        (assoc 0 (let [<> (nth xs 0)]
;;                   (reverse <>))))))
;; => true


code:
(ns jumpluff.datasets
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.utils :refer [log]])
  (:require-macros [cljs.core.async.macros :as async-macros]
                   [jumpluff.datasets :refer [update-vec-from-index-map]]))

(def fs (js/require "fs"))
(def csv (js/require "csv"))

(defn- parse-csv-vec
  "Validate and translate `vec` by each provided index"
  [columns vec]
  (update-vec-from-index-map
   vec columns <>
   :index-nicknames (if (and (not-empty <>)
                             (string? <>))
                      (->> (s/split <> ",")
                         (map s/trim)
                         (remove empty?)
                         (map s/lower-case)))
   :index-gps (->> <>
                 (re-find #"(-?\d{1,2}\.\d*,-?\d{1,2}\.\d*)")
                 second)
   :index-description (s/replace (or <> "") #"／" "\n")
   :index-ex (when-let [ex-str (and (string? <>)
                                    (not-empty <>)
                                    (s/upper-case <>))]
               (if (s/includes? ex-str "EX-T")
                 :ex-triggered
                 :ex-possible))))

(defn- load-csv-file [filename]
  (let [fs-chan (async/chan)
        csv-chan (async/chan)]
    (async-macros/go
      (.readFile fs filename #(async/put! fs-chan (or %2 "")))
      (.parse csv (async/<! fs-chan)
              #(async/put! csv-chan (js->clj (or %2 []))))
      (async/close! fs-chan))
    csv-chan))

(defn load-dataset
  "Load a dataset into `csv-files`"
  [csv-files base-dir [name {:keys [columns source]}]]
  (async-macros/go
    (swap! csv-files assoc source
           (map #(parse-csv-vec columns %)
                (rest (async/<! (->> source
                                   (str base-dir "/")
                                   load-csv-file)))))
    (log "Loaded dataset " name ":" source " containing "
         (count (get @csv-files source))
         " points")))


code:
(ns jumpluff.poi
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.state :as state]
            [jumpluff.utils :refer [vecs->maps
                                    dataset-names
                                    dataset-names-some-exact-match]]))

(def fuse (js/require "fuse.js"))

(defn- fuse-search-clj->js->clj
  [opts query collection]
  (js->clj (.search (fuse. (clj->js collection)
                           (clj->js opts))
                    query)))

(defn- fuse-search
  "Use fuse.js to fuzzy match a collection.
  The collection will go through `clj->js` and `js->clj`, losing
  keywords to strings."
  ([query collection]
   (when (every? string? collection)
     (when-let [vec-idx
                (not-empty
                 (fuse-search-clj->js->clj {:shouldSort true
                                            :threshold 0.3
                                            :location 0
                                            :distance 100
                                            :maxPatternLength 32
                                            :minMatchCharLength 2
                                            :tokenize true :matchAllTokens true}
                                           query collection))]
       (map #(nth collection %) vec-idx))))
  ;; with a custom getfn like nth to search by index
  ([keys getfn coll query]
   (fuse-search-clj->js->clj
    {:shouldSort true
     :threshold 0.2
     :location 0
     :distance 100
     :maxPatternLength 32
     :minMatchCharLength 2
     :tokenize true :matchAllTokens true
     :keys keys
     :getFn getfn}
    query coll)))

(defn- fuse-search--restore-keywords
  "Restore the :ex column to a keyword from a string"
  [js-vecs {:keys [index-ex]}]
  (map (fn [vec]
         (if (int? index-ex)
           (assoc vec index-ex
                  (keyword (nth vec index-ex)))
           vec))
       js-vecs))

(defn- split-query
  "Convoluted parser
  dataset name : name filter - description filter (-ex|+ex|+ex-t)"
  [query]
  (let [[query ex-keyword]
        (or (and (s/ends-with? query "+ex-t")
                 [(s/replace query #"\+ex-t$" "")
                  :ex-triggered])
            (and (s/ends-with? query "+ex")
                 [(s/replace query #"\+ex$" "")
                  :ex-possible])
            (and (s/ends-with? query "-ex")
                 [(s/replace query #"-ex$" "")
                  false])
            [query nil])]
    (let [split-on-colon (s/split query #":" 2)
          split-on-dash (s/split (last split-on-colon) #"-" -1)
          dataset-query (if (< 1 (count split-on-colon))
                          (s/trim (first split-on-colon)))
          name-query (cond (= 1 (count split-on-dash)) (s/trim (first split-on-dash))
                           (< 1 (count split-on-dash))
                           (s/trim (s/join "-" (drop-last split-on-dash)))
                           :else "")
          description-query (if-not (= 1 (count split-on-dash))
                              (s/trim (or (last split-on-dash) "")))]
      [dataset-query
       name-query
       description-query
       ex-keyword])))

(defn- poi-filter-by-nickname
  [name columns vec-collection]
  (when (int? (:index-nicknames columns))
    (let [nick (s/lower-case name)]
      (filter #(some #{nick} (nth % (:index-nicknames columns)))
              vec-collection))))

(defn s-replace-recur
  [st coll]
  (if (not-empty coll)
    (let [[re replacement] (first (take 1 coll))]
      (s-replace-recur (s/replace st re replacement)
                       (drop 1 coll)))
    st))

(defn sanitize-abbrv-map
  "Transform a map of keywords to strings to a nested vector of
  sanitized regexps to sanitized replacements."
  [m]
  (->> m
     (map (fn [[abbrv expansion]]
            (let [filter-str #(apply str (re-seq %1 (apply str (take 200 %2))))
                  re (when-let [abbrv-alpha (filter-str #"[a-zA-Z]" (name abbrv))]
                       (when (not-empty abbrv-alpha)
                         (re-pattern (str "(?i)\\b" abbrv-alpha "\\b"))))
                  replacement (not-empty (filter-str #"[\w\s\d]" (name expansion)))]
              (when (and re replacement)
                [re replacement]))))
     (remove nil?)))

(defn poi-name-abbrvs
  [name dataset-name]
  (or (some->> dataset-name
               (get @state/dataset-descriptions)
               :abbrv
               sanitize-abbrv-map
               (s-replace-recur name))
      name))

(defn- poi-filter-by--fuse
  [query filter-each index-key columns vec-collection]
  (when (int? (index-key columns))
    (fuse-search--restore-keywords
     (fuse-search [{:name (index-key columns)}]
                  #(filter-each (nth %1 %2 ""))
                  vec-collection
                  (filter-each query))
     columns)))

(defn- poi-filter-by-name
  [name columns vec-collection]
  (poi-filter-by--fuse name
                       #(s/replace % #"-|'|\." "")
                       :index-name
                       columns
                       vec-collection))

(defn- poi-filter-by-description
  [description-query columns vec-collection]
  (poi-filter-by--fuse description-query
                       #(s/replace % #"(?i)(?:\band\b|&)" "")
                       :index-description
                       columns
                       vec-collection))

(defn- poi-filter-by-ex
  [ex-keyword columns vec-collection]
  (when (int? (:index-ex columns))
    (filter (cond (= :ex-triggered ex-keyword)
                  #(= ex-keyword (nth % (:index-ex columns)))
                  (keyword? ex-keyword)
                  #(nth % (:index-ex columns))
                  :else
                  #(not (nth % (:index-ex columns))))
            vec-collection)))

(defn poi-search
  "Filter points of interest by QUERY.
  Use DATASET-NAME-FALLBACK to find a collection to filter if one was
  not named in the query.  When unable to find a collection the result
  will be nil, otherwise if nothing was found the result will be an
  empty vector."
  [dataset-name-fallback query]
  (let [[dataset-query name-query description-query ex-keyword] (split-query (or query ""))
        dataset-name (or (when (not-empty dataset-query)
                           (first (fuse-search dataset-query (dataset-names))))
                         dataset-name-fallback)]
    (when (dataset-names-some-exact-match dataset-name)
      (let [conf (get @state/dataset-descriptions dataset-name)
            vec-coll (get @state/csv-files (:source conf))
            columns (:columns conf)
            search-split-by-re
            (fn [re query coll]
              (when (re-find re query)
                (let [[name desc] (map s/trim (s/split query re))]
                  (when (and (not-empty name) (not-empty desc))
                    (some->> (not-empty (or (not-empty (poi-filter-by-nickname name columns coll))
                                            (not-empty (poi-filter-by-name
                                                        (poi-name-abbrvs name dataset-name) columns coll))))
                             (poi-filter-by-description desc columns))))))]
        (vecs->maps
         {:name (:index-name columns)
          :gps (:index-gps columns)
          :description (:index-description columns)
          :ex (:index-ex columns)
          :nicknames (:index-nicknames columns)}
         (cond->> vec-coll
           ;; name
           (not-empty name-query)
           ((fn [<>]
              (or (not-empty (poi-filter-by-nickname name-query columns <>))
                  (not-empty (poi-filter-by-name (poi-name-abbrvs name-query dataset-name) columns <>))
                  ;; Fallback: If description-query is empty run poi-filter-by-description with name-query
                  ;; if still nothing, try searching nick/name and description split by 'on'
                  (when (not (not-empty description-query))
                    (or (not-empty (poi-filter-by-description name-query columns <>))
                        (not-empty (search-split-by-re #"(?i)\bon\b" name-query <>))
                        (search-split-by-re #"(?i)\bat\b" name-query <>)))
                  [])))
           ;; desc
           (not-empty description-query)
           (poi-filter-by-description description-query columns)
           ;; ex keywords
           (not (nil? ex-keyword))
           (poi-filter-by-ex ex-keyword columns)))))))