diff --git a/plugins/fb_app/Gemfile b/plugins/fb_app/Gemfile new file mode 100644 index 0000000..34d0aa2 --- /dev/null +++ b/plugins/fb_app/Gemfile @@ -0,0 +1,10 @@ +gem 'slim' + +# for backwards compatibility of serialized objects +gem 'fb_graph' + +gem 'fb_graph2' + +gem 'facebook-signed-request' + + diff --git a/plugins/fb_app/config.yml.dist b/plugins/fb_app/config.yml.dist new file mode 100644 index 0000000..82eea62 --- /dev/null +++ b/plugins/fb_app/config.yml.dist @@ -0,0 +1,47 @@ +page_tab: + use_test_app: false +timeline: + use_test_app: true + +test_users: + - identifier1 + - identifier1 + +app: + id: xxx + secret: xxx + domain: domainconfigured.net + + open_graph: + namespace: app_name + objects: + blog_post: article + community: community + enterprise: sse_initiative + favorite_enterprise: sse_initiative + forum: discussion + event: event + friend: friend + gallery_image: picture + person: user + product: sse_product + uploaded_file: document + actions: + add: add + comment: comment + create: create + favorite: favorite + like: like + make_friendship: make_friendship + upload: upload + update: update + start: start + announce_creation: announce_creation + announce_new: announce_new + announce_update: announce_update + announce_news: announce_news + +test_app: + test_id: xxx + test_secret: xxx + diff --git a/plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb b/plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb new file mode 100644 index 0000000..5effb23 --- /dev/null +++ b/plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb @@ -0,0 +1,59 @@ +class FbAppPluginMyprofileController < OpenGraphPlugin::MyprofileController + + no_design_blocks + + before_filter :load_provider + before_filter :load_auth + + def index + if params[:tabs_added] + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added], params[:page_tab] + @page_tab = @page_tabs.first + redirect_to @page_tab.facebook_url + end + end + + def show_login + @status = params[:auth].delete :status + @logged_auth = FbAppPlugin::Auth.new params[:auth] + @logged_auth.fetch_user + if @auth.connected? + render partial: 'identity', locals: {auth: @logged_auth} + else + render nothing: true + end + end + + def save_auth + @status = params[:auth].delete :status rescue FbAppPlugin::Auth::Status::Unknown + if @status == FbAppPlugin::Auth::Status::Connected + @auth.attributes = params[:auth] + @auth.save! if @auth.changed? + else + @auth.destroy if @auth and @auth.persisted? + @auth = new_auth + end + + render partial: 'settings' + end + + protected + + def load_provider + @provider = FbAppPlugin.oauth_provider_for environment + end + + def load_auth + @auth = FbAppPlugin::Auth.where(profile_id: profile.id, provider_id: @provider.id).first + @auth ||= new_auth + end + + def new_auth + FbAppPlugin::Auth.new profile: profile, provider: @provider + end + + def context + :fb_app + end + +end diff --git a/plugins/fb_app/controllers/public/fb_app_plugin_controller.rb b/plugins/fb_app/controllers/public/fb_app_plugin_controller.rb new file mode 100644 index 0000000..e6753dc --- /dev/null +++ b/plugins/fb_app/controllers/public/fb_app_plugin_controller.rb @@ -0,0 +1,24 @@ +class FbAppPluginController < PublicController + + no_design_blocks + + def index + end + + def myprofile_config + if logged_in? + redirect_to controller: :fb_app_plugin_myprofile, profile: user.identifier + else + redirect_to controller: :account, action: :login, return_to: url_for(controller: :fb_app_plugin, action: :myprofile_config) + end + end + + protected + + # prevent session reset because X-CSRF not being passed by FB + # see also https://gist.github.com/toretore/911886 + def handle_unverified_request + end + +end + diff --git a/plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb b/plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb new file mode 100644 index 0000000..39c2239 --- /dev/null +++ b/plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb @@ -0,0 +1,173 @@ +class FbAppPluginPageTabController < FbAppPluginController + + no_design_blocks + + before_filter :change_theme + before_filter :disable_cache + + include CatalogHelper + + helper ManageProductsHelper + helper FbAppPlugin::DisplayHelper + + def index + return unless load_page_tabs + + if params[:tabs_added] + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added] + @page_tab = @page_tabs.first + redirect_to @page_tab.facebook_url + elsif @signed_request or @page_id + if @page_tab.present? + if product_id = params[:product_id] + @product = environment.products.find product_id + @profile = @product.profile + @inputs = @product.inputs + @allowed_user = false + load_catalog + + render action: 'product' + elsif @page_tab.config_type.in? [:profile, :own_profile] + @profile = @page_tab.value + + load_catalog + render action: 'catalog' unless performed? + else + # fake profile for catalog controller + @profile = environment.enterprise_default_template + @profile.shopping_cart_settings.enabled = true + + base_query = @page_tab.value + params[:base_query] = base_query + params[:scope] = 'all' + + load_catalog + render action: 'catalog' unless performed? + end + else + render action: 'first_load' + end + else + # render template + render action: 'index' + end + end + + def search_autocomplete + load_page_tabs + load_search_autocomplete + respond_to do |format| + format.json{ render 'catalog/search_autocomplete' } + end + end + + def admin + return redirect_to '/plugin/fb_app/myprofile_config' if params[:page_id].blank? and params[:signed_request].blank? + return unless load_page_tabs + + if request.put? and @page_id.present? + create_page_tabs if @page_tab.nil? + + @page_tab.update_attributes! params[:page_tab] + + respond_to do |format| + format.js{ render action: 'admin' } + end + end + end + + def destroy + @page_tab = FbAppPlugin::PageTab.find params[:id] + return render_access_denied unless user.present? and (user.is_admin?(environment) or user.is_admin? @page_tab.profile) + @page_tab.destroy + render nothing: true + end + + def uninstall + render text: params.to_yaml + end + + def enterprise_search + scope = environment.enterprises.enabled.public + @query = params[:query] + @profiles = scope.limit(10).order('name ASC'). + where(['name ILIKE ? OR name ILIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"]) + render partial: 'open_graph_plugin/myprofile/profile_search', locals: {profiles: @profiles} + end + + # unfortunetely, this needs to be public + def profile + @profile + end + + protected + + def default_url_options + {profile: @profile.identifier} if @profile + end + + def load_page_tabs + @signed_requests = read_param params[:signed_request] + if @signed_requests.present? + @datas = [] + @page_ids = @signed_requests.map do |signed_request| + @data = FbAppPlugin::Auth.parse_signed_request signed_request + @datas << @data + page_id = @data[:page][:id] rescue nil + if page_id.blank? + render_not_found + return false + end + page_id + end + else + @page_ids = read_param params[:page_id] + end + + @page_tabs = FbAppPlugin::PageTab.where page_id: @page_ids + + @signed_request = @signed_requests.first + @page_id = @page_ids.first + @page_tab = @page_tabs.first + @new_request = @page_tab.blank? + + true + end + + def create_page_tabs + @page_tabs = FbAppPlugin::PageTab.create_from_page_ids @page_ids + @page_tab ||= @page_tabs.first + end + + def change_theme + # move to config + unless theme_responsive? + @current_theme = 'ees' + @theme_responsive = true + end + @without_pure_chat = true + end + def get_layout + return nil if request.format == :js or request.xhr? + + return 'application-responsive' + end + + def disable_cache + @disable_cache_theme_navigation = true + end + + def load_catalog options = {} + @use_show_more = true + catalog_load_index options + end + + def read_param param + if param.is_a? Hash + param.values + else + Array(param).select{ |p| p.present? } + end + end + +end diff --git a/plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb b/plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb new file mode 100644 index 0000000..d5a7000 --- /dev/null +++ b/plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb @@ -0,0 +1,16 @@ +class CreateFbAppPageTabConfig < ActiveRecord::Migration + + def change + create_table :fb_app_plugin_page_tab_configs do |t| + t.string :page_id + t.text :config, default: {}.to_yaml + t.integer :profile_id + + t.timestamps + end + add_index :fb_app_plugin_page_tab_configs, [:profile_id] + add_index :fb_app_plugin_page_tab_configs, [:page_id] + add_index :fb_app_plugin_page_tab_configs, [:page_id, :profile_id] + end + +end diff --git a/plugins/fb_app/install.rb b/plugins/fb_app/install.rb new file mode 100644 index 0000000..158c8f0 --- /dev/null +++ b/plugins/fb_app/install.rb @@ -0,0 +1,3 @@ +system "script/noosfero-plugins -q enable oauth_client" +system "script/noosfero-plugins -q enable open_graph" + diff --git a/plugins/fb_app/lib/ext/profile.rb b/plugins/fb_app/lib/ext/profile.rb new file mode 100644 index 0000000..c2a753e --- /dev/null +++ b/plugins/fb_app/lib/ext/profile.rb @@ -0,0 +1,28 @@ +require_dependency 'profile' +# hate to wrte this, but without Noosfero::Plugin::Settings is loaded instead +require 'fb_app_plugin/settings' + +# attr_accessible must be defined on subclasses +Profile.descendants.each do |subclass| + subclass.class_eval do + attr_accessible :fb_app_settings + end +end + +class Profile + + def fb_app_settings attrs = {} + @fb_app_settings ||= FbAppPlugin::Settings.new self, attrs + attrs.each{ |a, v| @fb_app_settings.send "#{a}=", v } + @fb_app_settings + end + alias_method :fb_app_settings=, :fb_app_settings + + has_many :fb_app_page_tabs, class_name: 'FbAppPlugin::PageTab' + + def fb_app_auth + provider = FbAppPlugin.oauth_provider_for self.environment + self.oauth_auths.where(provider_id: provider.id).first + end + +end diff --git a/plugins/fb_app/lib/fb_app_plugin.rb b/plugins/fb_app/lib/fb_app_plugin.rb new file mode 100644 index 0000000..2fbe074 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin.rb @@ -0,0 +1,87 @@ +module FbAppPlugin + + extend Noosfero::Plugin::ParentMethods + + def self.plugin_name + I18n.t 'fb_app_plugin.lib.plugin.name' + end + + def self.plugin_description + I18n.t 'fb_app_plugin.lib.plugin.description' + end + + def self.config + @config ||= HashWithIndifferentAccess.new(YAML.load File.read("#{File.dirname __FILE__}/../config.yml")) rescue {} + end + + def self.test_users + @test_users ||= self.config[:test_users] + end + def self.test_user? user + user and (self.test_users.blank? or self.test_users.include? user.identifier) + end + + def self.debug? actor=nil + self.test_user? actor + end + + def self.scope user + if self.test_user? user then 'publish_actions' else '' end + end + + def self.oauth_provider_for environment + return unless self.config.present? + + @oauth_providers ||= {} + @oauth_providers[environment] ||= begin + app_id = self.timeline_app_credentials[:id].to_s + app_secret = self.timeline_app_credentials[:secret].to_s + + client = environment.oauth_providers.where(client_id: app_id).first + # attributes that may be changed by the user + client ||= OauthClientPlugin::Provider.new strategy: 'facebook', + name: 'FB App', site: 'https://facebook.com' + + # attributes that should not change + client.attributes = { + client_id: app_id, client_secret: app_secret, + environment_id: environment.id, + } + client.save! if client.changed? + + client + end + end + + def self.open_graph_config + return unless self.config.present? + + @open_graph_config ||= begin + key = if self.config[:timeline][:use_test_app] then :test_app else :app end + self.config[key][:open_graph] + end + end + + def self.credentials app = :app + return unless self.config.present? + {id: self.config[app][:id], secret: self.config[app][:secret]} + end + + def self.timeline_app_credentials + return unless self.config.present? + @timeline_app_credentials ||= begin + key = if self.config[:timeline][:use_test_app] then :test_app else :app end + self.credentials key + end + end + + def self.page_tab_app_credentials + return unless self.config.present? + @page_tab_app_credentials ||= begin + key = if self.config[:page_tab][:use_test_app] then :test_app else :app end + self.credentials key + end + end + +end + diff --git a/plugins/fb_app/lib/fb_app_plugin/base.rb b/plugins/fb_app/lib/fb_app_plugin/base.rb new file mode 100644 index 0000000..fdb2238 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin/base.rb @@ -0,0 +1,35 @@ +class FbAppPlugin::Base < Noosfero::Plugin + + def stylesheet? + true + end + + def js_files + ['fb_app.js'].map{ |j| "javascripts/#{j}" } + end + + def head_ending + return unless FbAppPlugin.config.present? + lambda do + tag 'meta', property: 'fb:app_id', content: FbAppPlugin.config[:app][:id] + end + end + + def control_panel_buttons + return unless FbAppPlugin.config.present? + { title: FbAppPlugin.plugin_name, icon: 'fb-app', url: {host: FbAppPlugin.config[:app][:domain], profile: profile.identifier, controller: :fb_app_plugin_myprofile} } + end + +end + +ActiveSupport.on_load :open_graph_plugin do + OpenGraphPlugin::Stories.register_publisher FbAppPlugin::Publisher.default +end +ActiveSupport.on_load :metadata_plugin do + MetadataPlugin::Controllers.class_eval do + def fb_app_plugin_page_tab + :@product + end + end +end + diff --git a/plugins/fb_app/lib/fb_app_plugin/display_helper.rb b/plugins/fb_app/lib/fb_app_plugin/display_helper.rb new file mode 100644 index 0000000..e7c9276 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin/display_helper.rb @@ -0,0 +1,51 @@ +module FbAppPlugin::DisplayHelper + + extend CatalogHelper + + def fb_url_options options + options.merge! page_id: @page_ids, signed_request: @signed_requests, id: nil + end + + def url_for options = {} + return super unless options.is_a? Hash + if options[:controller] == :catalog + options[:controller] = :fb_app_plugin_page_tab + options = fb_url_options options + end + super + end + + protected + + def product_url_options product, options = {} + options = options.merge! product.url + options = options.merge! controller: :fb_app_plugin_page_tab, product_id: product.id, action: :index + options = fb_url_options options + unless Rails.env.development? + domain = FbAppPlugin.config[:app][:domain] + options[:host] = domain if domain.present? + options[:protocol] = '//' + end + options + end + def product_path product, options = {} + url = url_for product_url_options(product, options = {}) + url + end + + def link_to_product product, opts = {} + url_opts = opts.delete(:url_options) || {} + url_opts = product_url_options product, url_opts + url = params.merge url_opts + link_to content_tag('span', product.name), url, + opts.merge(target: '') + end + + def link_to name = nil, options = nil, html_options = nil, &block + html_options ||= {} + options[:protocol] = '//' if options.is_a? Hash + html_options[:target] ||= '_parent' + super + end + +end diff --git a/plugins/fb_app/lib/fb_app_plugin/link_renderer.rb b/plugins/fb_app/lib/fb_app_plugin/link_renderer.rb new file mode 100644 index 0000000..405d605 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin/link_renderer.rb @@ -0,0 +1,14 @@ +# add target attribute to links +class FbAppPlugin::LinkRenderer < WillPaginate::ActionView::LinkRenderer + + def prepare collection, options, template + super + end + + protected + + def default_url_params + {target: ''} + end + +end diff --git a/plugins/fb_app/lib/fb_app_plugin/publisher.rb b/plugins/fb_app/lib/fb_app_plugin/publisher.rb new file mode 100644 index 0000000..c390d57 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin/publisher.rb @@ -0,0 +1,17 @@ +# Publishing examples on console +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; a=Article.find 307591 +# pub.publish_story a, u, :announce_news_from_a_sse_initiative +# +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; f=FavoriteEnterprisePerson.last +# pub.publish_story f, u, :favorite_a_sse_initiative +# +class FbAppPlugin::Publisher < OpenGraphPlugin::Publisher + + def publish_story object_data, actor, story + OpenGraphPlugin.context = FbAppPlugin::Activity.context + a = FbAppPlugin::Activity.new object_data: object_data, actor: actor, story: story + a.dispatch_publications + a.save + end + +end diff --git a/plugins/fb_app/lib/fb_app_plugin/settings.rb b/plugins/fb_app/lib/fb_app_plugin/settings.rb new file mode 100644 index 0000000..54e5f53 --- /dev/null +++ b/plugins/fb_app/lib/fb_app_plugin/settings.rb @@ -0,0 +1,4 @@ +class FbAppPlugin::Settings < OpenGraphPlugin::Settings + +end + diff --git a/plugins/fb_app/locales/en-US.yml b/plugins/fb_app/locales/en-US.yml new file mode 100644 index 0000000..5aa489a --- /dev/null +++ b/plugins/fb_app/locales/en-US.yml @@ -0,0 +1,83 @@ + +"en-US": &en-US + + fb_app_plugin: + lib: + plugin: + name: 'Facebook integration' + description: 'Use the app for Facebook!' + models: + page_tab: + types: + own_profile: 'Catalog from this enterprise' + profile: 'Catalog from a single SSE enterprise' + other_profile: 'Catalog from other SSE enterprise' + profiles: 'Catalog from more than one SSE enterprise' + query: 'Catalog of products chosen by filter or free search' + views: + myprofile: + checking_auth: 'Checking authorization' + different_login: 'But you are logged in on facebook as:' + current_login: 'You are logged in on facebook as:' + connect: 'I want to install the app for Facebook' + logged_connect: 'Connect this Facebook account with my user %{profile}' + disconnect: 'Disconnect' + connect_to_another: 'Conect to another Facebook account' + reconnect: 'Reconnect' + + timeline: + heading: 'Publishing in your facebook' + add: 'Save timeline post settings' + explanation_title: 'Soon!' + explanation_text: 'In a short time, your actions will become new posts in your Facebook timeline!' + organization_redirect: '%{redirect_link} to post updates %{type}, conect your personal profile to Facebook.' + organization_from_enterprise: 'from this enterprise' + organization_from_community: 'from this community' + redirect_link: 'Click here' + + catalogs: + heading: 'Solidarity Economy Catalog' + new: 'Add new catalog' + catalog_title_label: 'Title' + catalog_subtitle_label: 'Subtitle' + catalog_type_chooser_label: 'Type' + profile_chooser_label: 'Enterprise' + profiles_chooser_label: 'Enterprises' + query_label: "Criteria for the catalog's products" + query_help: "Write the words separated by space, and click at the button below to save." + edit_button: "Edit catalog '%{catalog_title}'" + remove_button: "Remove catalog '%{catalog_title}'" + cancel_button: 'Cancel' + confirm_removal: "

Warning: To really remove a catalog, you must go to the facebook page where it is, click at 'add or remove tabs' and then remove the tab.

After that, you can come here and remove the catalog from our settings.

Are you sure you want to remove this catalog?

" + confirm_removal_button: 'Yes, I want to delete this catalog' + confirm_disconnect: "

Warning: If you disconnect your account to this Facebook profile, all your catalogs will also be removed

And then, to really remove your catalogs (if they were created), you must go to the facebook page where it is, click at 'add or remove tabs' and then remove the tab.

After that, you can come here and disconnect.

Are you sure you want to disconnect your account to this Facebook profile?

" + confirm_disconnect_button: 'Yes, I want to disconnect my account from this Facebook profile' + + catalog: + see_page: 'See page on Facebook' + + error: + empty_title: 'Please add a title to your catalog.' + empty_settings: 'Please choose SSE enterprises or search terms or filters for your catalog.' + + page_tab: + edit_catalog: 'Edit catalog' + add: 'Add catalog to one of my pages on Facebook' + save: 'Save' + added_notice: "Congratulations: You've just published a new SSE catalog in Facebook!" + profile: + placeholder: "select an enterprise" + profiles: + placeholder: "select the enterprises" + query: + placeholder: "select the category or type the search terms" + back_to_catalog: 'Back to catalog' + footer1: "" + footer2: "" + + +'en_US': + <<: *en-US +'en': + <<: *en-US + diff --git a/plugins/fb_app/locales/pt-BR.yml b/plugins/fb_app/locales/pt-BR.yml new file mode 100644 index 0000000..959d6a5 --- /dev/null +++ b/plugins/fb_app/locales/pt-BR.yml @@ -0,0 +1,82 @@ + +"pt-BR": &pt-BR + + fb_app_plugin: + lib: + plugin: + name: 'App Facebook' + description: 'Divulgue suas ações no Facebook!' + models: + page_tab: + types: + own_profile: 'Vitrine deste empreendimento' + profile: 'Vitrine de um único empreendimento' + other_profile: 'Vitrine de outro empreendimento' + profiles: 'Vitrine de mais de um empreendimento' + query: 'Vitrine de produtos ou serviços escolhidos por busca livre' + views: + myprofile: + checking_auth: 'Verificando autorização de acesso' + different_login: 'Mas você está logado no facebook como:' + current_login: 'Você está logado no facebook como:' + connect: 'Quero instalar o App Facebook' + logged_connect: 'Quero conectar esta conta do Facebook com meu usuário %{profile}' + disconnect: 'Desconectar' + connect_to_another: 'Conectar a outra conta do Facebook' + reconnect: 'Reconectar' + + timeline: + heading: 'Postar ações no facebook automaticamente:' + add: 'Salvar configuração de postagens na timeline' + explanation_title: 'Aguarde!' + explanation_text: 'Em breve, este aplicativo poderá postar automaticamente no seu face as ações que você fizer! Por exemplo: quando você postar novos conteúdos no seu blog, enviar uma imagem, etc.' + organization_redirect: '%{redirect_link} para postar atualizações %{type}, conecte o seu perfil pessoal ao facebook.' + organization_from_enterprise: 'deste empreendimento' + organization_from_community: 'desta comunidade' + redirect_link: 'Clique aqui' + + catalogs: + heading: 'Vitrine' + new: 'Criar nova vitrine' + catalog_title_label: 'Título' + catalog_subtitle_label: 'Subtítulo' + catalog_type_chooser_label: 'Tipo' + profile_chooser_label: 'Empreendimento' + profiles_chooser_label: 'Empreendimentos' + query_label: "Critérios para os produtos/serviços da vitrine" + query_help: "Escreva as palavras separadas por espaço, e clique no botão abaixo para salvar." + edit_button: "Editar vitrine '%{catalog_title}'" + remove_button: "Remover vitrine '%{catalog_title}'" + cancel_button: 'Cancelar' + confirm_removal: "

Atenção: Para realmente apagar uma vitrine, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:

Depois disso, você pode vir aqui e pedir para remover a vitrine.

Se você já removeu lá no Facebook, tem certeza que quer remover esta vitrine?

" + confirm_removal_button: 'Sim, quero remover esta vitrine' + confirm_disconnect: "

Atenção: Se você desconectar, vai apagar todas as vitrines. Portanto, para realmente apagar suas vitrines, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:

Depois disso, você pode vir aqui e desconectar.

Tem certeza que quer desconectar?

" + confirm_disconnect_button: 'Sim, quero desconectar' + + catalog: + see_page: 'Ver página no facebook' + + error: + empty_title: 'Por favor, coloque um título para a sua vitrine.' + empty_settings: 'Por favor, selecione os empreendimentos solidários ou os termos de busca para sua vitrine.' + + page_tab: + edit_catalog: 'Editar vitrine' + add: 'Criar vitrine em uma página sua no Facebook' + save: 'Salvar' + added_notice: 'Parabéns: você acaba de publicar uma nova vitrine da Economia Solidária no Facebook!' + profile: + placeholder: "selecione um empreendimento" + profiles: + placeholder: "selecione os empreendimentos" + query: + placeholder: "escolha os produtos e serviços por palavras de busca" + back_to_catalog: 'Voltar à vitrine' + footer1: "" + footer2: "" + +'pt_BR': + <<: *pt-BR +'pt': + <<: *pt-BR + diff --git a/plugins/fb_app/models/fb_app_plugin/activity.rb b/plugins/fb_app/models/fb_app_plugin/activity.rb new file mode 100644 index 0000000..cff182b --- /dev/null +++ b/plugins/fb_app/models/fb_app_plugin/activity.rb @@ -0,0 +1,54 @@ +class FbAppPlugin::Activity < OpenGraphPlugin::Activity + + self.context = :fb_app + self.actions = FbAppPlugin.open_graph_config[:actions] + self.objects = FbAppPlugin.open_graph_config[:objects] + + # this avoid to many saves for frequent fail cases + attr_accessor :should_save + validates_presence_of :should_save + + def self.scrape object_data_url + params = {id: object_data_url, scrape: true, method: 'post'} + url = "http://graph.facebook.com?#{params.to_query}" + Net::HTTP.get URI.parse(url) + end + def scrape + self.class.scrape self.object_data_url + end + + def publish! actor = self.actor + print_debug "fb_app: action #{self.action}, object_type #{self.object_type}" if debug? actor + + auth = actor.fb_app_auth + return if auth.blank? or auth.expired? + print_debug "fb_app: Auth found and is valid" if debug? actor + + # always update the object to expire facebook cache + Thread.new{ self.scrape } + + return if self.defs[:on] == :update and self.recent_publish? actor, self.object_type, self.object_data_url + print_debug "fb_app: no recent publication found, making new" if debug? actor + + self.should_save = true + + namespace = FbAppPlugin.open_graph_config[:namespace] + # to_str is needed to ensure String, see https://github.com/nov/fb_graph2/issues/88 + params = {self.object_type => self.object_data_url.to_str} + params['fb:explicitly_shared'] = 'true' unless self.defs[:tracker] + print_debug "fb_app: publishing with params #{params.inspect}" if debug? actor + + me = FbGraph2::User.me auth.access_token + me.og_action! "#{namespace}:#{action}", params + + self.published_at = Time.now + print_debug "fb_app: published with success" if debug? actor + end + + protected + + def debug? actor=nil + super or FbAppPlugin.debug? actor + end + +end diff --git a/plugins/fb_app/models/fb_app_plugin/auth.rb b/plugins/fb_app/models/fb_app_plugin/auth.rb new file mode 100644 index 0000000..bf33253 --- /dev/null +++ b/plugins/fb_app/models/fb_app_plugin/auth.rb @@ -0,0 +1,89 @@ +class FbAppPlugin::Auth < OauthClientPlugin::Auth + + module Status + Connected = 'connected' + NotAuthorized = 'not_authorized' + Unknown = 'unknown' + end + + settings_items :signed_request + settings_items :fb_user + + attr_accessible :provider_user_id, :signed_request + + before_create :update_user + before_create :exchange_token + after_create :schedule_exchange_token + after_destroy :destroy_page_tabs + before_validation :set_enabled + + validates_presence_of :provider_user_id + validates_uniqueness_of :provider_user_id, scope: :profile_id + + def self.parse_signed_request signed_request, credentials = FbAppPlugin.page_tab_app_credentials + secret = credentials[:secret] rescue '' + request = Facebook::SignedRequest.new signed_request, secret: secret + request.data + end + + def status + if self.access_token.present? and self.not_expired? then Status::Connected else Status::NotAuthorized end + end + def not_authorized? + self.status == Status::NotAuthorized + end + def connected? + self.status == Status::Connected + end + + def exchange_token + app_id = FbAppPlugin.timeline_app_credentials[:id] + app_secret = FbAppPlugin.timeline_app_credentials[:secret] + fb_auth = FbGraph2::Auth.new app_id, app_secret + fb_auth.fb_exchange_token = self.access_token + + access_token = fb_auth.access_token! + self.access_token = access_token.access_token + self.expires_in = access_token.expires_in + # refresh user and its stored access token + self.fetch_user + end + + def exchange_token! + self.exchange_token + self.save! + end + + def signed_request_data + self.class.parse_signed_request self.signed_request + end + + def fetch_user + fb_user = FbGraph2::User.me self.access_token + self.fb_user = fb_user.fetch + end + def update_user + self.fb_user = self.fetch_user + end + + protected + + def destroy_page_tabs + self.profile.fb_app_page_tabs.destroy_all + end + + def exchange_token_and_reschedule! + self.exchange_token! + self.schedule_exchange_token + end + + def schedule_exchange_token + self.delay(run_at: self.expires_at - 2.weeks).exchange_token_and_reschedule! + end + + def set_enabled + self.enabled = self.not_expired? + end + +end + diff --git a/plugins/fb_app/models/fb_app_plugin/page_tab.rb b/plugins/fb_app/models/fb_app_plugin/page_tab.rb new file mode 100644 index 0000000..6f79bb8 --- /dev/null +++ b/plugins/fb_app/models/fb_app_plugin/page_tab.rb @@ -0,0 +1,111 @@ +class FbAppPlugin::PageTab < ActiveRecord::Base + + # FIXME: rename table to match model + self.table_name = :fb_app_plugin_page_tab_configs + + attr_accessible :owner_profile, :profile_id, :page_id, + :config_type, :profile_ids, :query, + :title, :subtitle + + belongs_to :owner_profile, foreign_key: :profile_id, class_name: 'Profile' + + acts_as_having_settings field: :config + + ConfigTypes = [:profile, :profiles, :query] + EnterpriseConfigTypes = [:own_profile] + ConfigTypes + + validates_presence_of :page_id + validates_uniqueness_of :page_id + validates_inclusion_of :config_type, in: ConfigTypes + EnterpriseConfigTypes + + def self.page_ids_from_tabs_added tabs_added + tabs_added.map{ |id, value| id } + end + + def self.create_from_page_ids page_ids, attrs = {} + attrs.delete :page_id + page_ids.map do |page_id| + page_tab = FbAppPlugin::PageTab.where(page_id: page_id).first + page_tab ||= FbAppPlugin::PageTab.new page_id: page_id + page_tab.update_attributes! attrs + page_tab + end + end + def self.create_from_tabs_added tabs_added, attrs = {} + page_ids = self.page_ids_from_tabs_added tabs_added + self.create_from_page_ids page_ids, attrs + end + + def self.facebook_url page_id + "https://facebook.com/#{page_id}?sk=app_#{FbAppPlugin.page_tab_app_credentials[:id]}" + end + + def facebook_url + self.class.facebook_url self.page_id + end + + def types + if self.owner_profile.present? and self.owner_profile.enterprise? then EnterpriseConfigTypes else ConfigTypes end + end + + def config_type + self.config[:type] || (self.owner_profile ? :own_profile : :profile) + end + def config_type= value + self.config[:type] = value.to_sym + end + + def value + case self.config_type + when :profiles + self.profiles.map(&:identifier).join(' OR ') + else + self.send self.config_type + end + end + def blank? + self.value.blank? rescue true + end + + def own_profile + self.owner_profile + end + def profiles + Profile.where(id: self.config[:profile_ids]) + end + def profile + self.profiles.first + end + def profile_ids + self.profiles.map(&:id) + end + def query + self.config[:query] + end + + def title + self.config[:title] + end + def title= value + self.config[:title] = value + end + + def subtitle + self.config[:subtitle] + end + def subtitle= value + self.config[:subtitle] = value + end + + def profile_ids= ids + ids = ids.to_s.split(',') + self.config[:type] = if ids.size == 1 then :profile else :profiles end + self.config[:profile_ids] = ids + end + + def query= value + self.config[:type] = :query + self.config[:query] = value + end + +end diff --git a/plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb b/plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb new file mode 100644 index 0000000..df4d462 --- /dev/null +++ b/plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb @@ -0,0 +1,12 @@ +require_dependency 'action_tracker_model' + +class ActionTracker::Record + + after_create :fb_app_publish + + protected + + def fb_app_publish + raise 'here' + end +end diff --git a/plugins/fb_app/public/images/FB-f-Logo__blue_48.png b/plugins/fb_app/public/images/FB-f-Logo__blue_48.png new file mode 100644 index 0000000..f49c3b4 Binary files /dev/null and b/plugins/fb_app/public/images/FB-f-Logo__blue_48.png differ diff --git a/plugins/fb_app/public/images/cirandasnoface.png b/plugins/fb_app/public/images/cirandasnoface.png new file mode 100644 index 0000000..068bd15 Binary files /dev/null and b/plugins/fb_app/public/images/cirandasnoface.png differ diff --git a/plugins/fb_app/public/images/control-panel.png b/plugins/fb_app/public/images/control-panel.png new file mode 120000 index 0000000..9aa1be9 --- /dev/null +++ b/plugins/fb_app/public/images/control-panel.png @@ -0,0 +1 @@ +FB-f-Logo__blue_48.png \ No newline at end of file diff --git a/plugins/fb_app/public/images/loading.gif b/plugins/fb_app/public/images/loading.gif new file mode 100644 index 0000000..260bb1d Binary files /dev/null and b/plugins/fb_app/public/images/loading.gif differ diff --git a/plugins/fb_app/public/javascripts/bootstrap-tokenfield.js b/plugins/fb_app/public/javascripts/bootstrap-tokenfield.js new file mode 100644 index 0000000..ae12c94 --- /dev/null +++ b/plugins/fb_app/public/javascripts/bootstrap-tokenfield.js @@ -0,0 +1,1032 @@ +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // For CommonJS and CommonJS-like environments where a window with jQuery + // is present, execute the factory with the jQuery instance from the window object + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a Tokenfield-making factory as module.exports + // This accentuates the need for the creation of a real window or passing in a jQuery instance + // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($); + module.exports = global.window && global.window.$ ? + factory( global.window.$ ) : + function( input ) { + if ( !input.$ && !input.fn ) { + throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" ); + } + return factory( input.$ || input ); + }; + } else { + // Browser globals + factory(jQuery, window); + } +}(function ($, window) { + + "use strict"; // jshint ;_; + + /* TOKENFIELD PUBLIC CLASS DEFINITION + * ============================== */ + + var Tokenfield = function (element, options) { + var _self = this + + this.$element = $(element) + this.textDirection = this.$element.css('direction'); + + // Extend options + this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options) + + // Setup delimiters and trigger keys + this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter + this._triggerKeys = $.map(this._delimiters, function (delimiter) { + return delimiter.charCodeAt(0); + }); + this._firstDelimiter = this._delimiters[0]; + + // Check for whitespace, dash and special characters + var whitespace = $.inArray(' ', this._delimiters) + , dash = $.inArray('-', this._delimiters) + + if (whitespace >= 0) + this._delimiters[whitespace] = '\\s' + + if (dash >= 0) { + delete this._delimiters[dash] + this._delimiters.unshift('-') + } + + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] + $.each(this._delimiters, function (index, character) { + var pos = $.inArray(character, specialCharacters) + if (pos >= 0) _self._delimiters[index] = '\\' + character; + }); + + // Store original input width + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null + , elStyleWidth = element.style.width + , elCSSWidth + , elWidth = this.$element.width() + + if (elRules) { + $.each( elRules, function (i, rule) { + if (rule.style.width) { + elCSSWidth = rule.style.width; + } + }); + } + + // Move original input out of the way + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', + originalStyles = { position: this.$element.css('position') }; + originalStyles[hidingPosition] = this.$element.css(hidingPosition); + + this.$element + .data('original-styles', originalStyles) + .data('original-tabindex', this.$element.prop('tabindex')) + .css('position', 'absolute') + .css(hidingPosition, '-10000px') + .prop('tabindex', -1) + + // Create a wrapper + this.$wrapper = $('
') + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') + + // Create a new input + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) + this.$input = $('') + .appendTo( this.$wrapper ) + .prop( 'placeholder', this.$element.prop('placeholder') ) + .prop( 'id', id + '-tokenfield' ) + .prop( 'tabindex', this.$element.data('original-tabindex') ) + + // Re-route original input label to new input + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$input.prop('id') ) + } + + // Set up a copy helper to handle copy & paste + this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) + + // Set wrapper width + if (elStyleWidth) { + this.$wrapper.css('width', elStyleWidth); + } + else if (elCSSWidth) { + this.$wrapper.css('width', elCSSWidth); + } + // If input is inside inline-form with no width set, set fixed width + else if (this.$element.parents('.form-inline').length) { + this.$wrapper.width( elWidth ) + } + + // Set tokenfield disabled, if original or fieldset input is disabled + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { + this.disable(); + } + + // Set tokenfield readonly, if original input is readonly + if (this.$element.prop('readonly')) { + this.readonly(); + } + + // Set up mirror for input auto-sizing + this.$mirror = $(''); + this.$input.css('min-width', this.options.minWidth + 'px') + $.each([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent' + ], function (i, val) { + _self.$mirror[0].style[val] = _self.$input.css(val); + }); + this.$mirror.appendTo( 'body' ) + + // Insert tokenfield to HTML + this.$wrapper.insertBefore( this.$element ) + this.$element.prependTo( this.$wrapper ) + + // Calculate inner input width + this.update() + + // Create initial tokens, if any + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) + + // Start listening to events + this.listen() + + // Initialize autocomplete, if necessary + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { + var side = this.textDirection === 'rtl' ? 'right' : 'left' + , autocompleteOptions = $.extend({ + minLength: this.options.showAutocompleteOnFocus ? 0 : null, + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } + }, this.options.autocomplete ) + + this.$input.autocomplete( autocompleteOptions ) + } + + // Initialize typeahead, if necessary + if ( ! $.isEmptyObject( this.options.typeahead ) ) { + + var typeaheadOptions = this.options.typeahead + , defaults = { + minLength: this.options.showAutocompleteOnFocus ? 0 : null + } + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] + + args[0] = $.extend( {}, defaults, args[0] ) + + this.$input.typeahead.apply( this.$input, args ) + this.typeahead = true + } + } + + Tokenfield.prototype = { + + constructor: Tokenfield + + , createToken: function (attrs, triggerChange) { + var _self = this + + if (typeof attrs === 'string') { + attrs = { value: attrs, label: attrs } + } else { + // Copy objects to prevent contamination of data sources. + attrs = $.extend( {}, attrs ) + } + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + // Normalize label and value + attrs.value = $.trim(attrs.value.toString()); + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value + + // Bail out if has no value or label, or label is too short + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return + + // Bail out if maximum number of tokens is reached + if (this.options.limit && this.getTokens().length >= this.options.limit) return + + // Allow changing token data before creating it + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) + this.$element.trigger(createEvent) + + // Bail out if there if attributes are empty or event was defaultPrevented + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return + + var $token = $('
') + .append('') + .append('×') + .data('attrs', attrs) + + // Insert token into HTML + if (this.$input.hasClass('tt-input')) { + // If the input has typeahead enabled, insert token before it's parent + this.$input.parent().before( $token ) + } else { + this.$input.before( $token ) + } + + // Temporarily set input width to minimum + this.$input.css('width', this.options.minWidth + 'px') + + var $tokenLabel = $token.find('.token-label') + , $closeButton = $token.find('.close') + + // Determine maximum possible token label width + if (!this.maxTokenWidth) { + this.maxTokenWidth = + this.$wrapper.width() - $closeButton.outerWidth() - + parseInt($closeButton.css('margin-left'), 10) - + parseInt($closeButton.css('margin-right'), 10) - + parseInt($token.css('border-left-width'), 10) - + parseInt($token.css('border-right-width'), 10) - + parseInt($token.css('padding-left'), 10) - + parseInt($token.css('padding-right'), 10) + parseInt($tokenLabel.css('border-left-width'), 10) - + parseInt($tokenLabel.css('border-right-width'), 10) - + parseInt($tokenLabel.css('padding-left'), 10) - + parseInt($tokenLabel.css('padding-right'), 10) + parseInt($tokenLabel.css('margin-left'), 10) - + parseInt($tokenLabel.css('margin-right'), 10) + } + + //$tokenLabel.css('max-width', this.maxTokenWidth) + if (this.options.html) + $tokenLabel.html(attrs.label) + else + $tokenLabel.text(attrs.label) + + // Listen to events on token + $token + .on('mousedown', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = true + }) + .on('click', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = false + + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + return _self.toggle( $token ) + } + + _self.activate( $token, e.shiftKey, e.shiftKey ) + }) + .on('dblclick', function (e) { + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false + _self.edit( $token ) + }) + + $closeButton + .on('click', $.proxy(this.remove, this)) + + // Trigger createdtoken event on the original field + // indicating that the token is now in the DOM + this.$element.trigger($.Event('tokenfield:createdtoken', { + attrs: attrs, + relatedTarget: $token.get(0) + })) + + // Trigger change event on the original field + if (triggerChange) { + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) + } + + // Update tokenfield dimensions + this.update() + + // Return original element + return this.$element.get(0) + } + + , setTokens: function (tokens, add, triggerChange) { + if (!tokens) return + + if (!add) this.$wrapper.find('.token').remove() + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + if (typeof tokens === 'string') { + if (this._delimiters.length) { + // Split based on delimiters + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) + } else { + tokens = [tokens]; + } + } + + var _self = this + $.each(tokens, function (i, attrs) { + _self.createToken(attrs, triggerChange) + }) + + return this.$element.get(0) + } + + , getTokenData: function($token) { + var data = $token.map(function() { + var $token = $(this); + return $token.data('attrs') + }).get(); + + if (data.length == 1) { + data = data[0]; + } + + return data; + } + + , getTokens: function(active) { + var self = this + , tokens = [] + , activeClass = active ? '.active' : '' // get active tokens only + this.$wrapper.find( '.token' + activeClass ).each( function() { + tokens.push( self.getTokenData( $(this) ) ) + }) + return tokens + } + + , getTokensList: function(delimiter, beautify, active) { + delimiter = delimiter || this._firstDelimiter + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify + + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') + return $.map( this.getTokens(active), function (token) { + return token.value + }).join(separator) + } + + , getInput: function() { + return this.$input.val() + } + + , listen: function () { + var _self = this + + this.$element + .on('change', $.proxy(this.change, this)) + + this.$wrapper + .on('mousedown',$.proxy(this.focusInput, this)) + + this.$input + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('paste', $.proxy(this.paste, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + this.$copyHelper + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Secondary listeners for input width calculation + this.$input + .on('keypress', $.proxy(this.update, this)) + .on('keyup', $.proxy(this.update, this)) + + this.$input + .on('autocompletecreate', function() { + // Set minimum autocomplete menu width + var $_menuElement = $(this).data('ui-autocomplete').menu.element + + var minWidth = _self.$wrapper.outerWidth() - + parseInt( $_menuElement.css('border-left-width'), 10 ) - + parseInt( $_menuElement.css('border-right-width'), 10 ) + + $_menuElement.css( 'min-width', minWidth + 'px' ) + }) + .on('autocompleteselect', function (e, ui) { + if (_self.createToken( ui.item )) { + _self.$input.val('') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + return false + }) + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { + // Create token + if (_self.createToken( datum )) { + _self.$input.typeahead('val', '') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + }) + + // Listen to window resize + $(window).on('resize', $.proxy(this.update, this )) + + } + + , keydown: function (e) { + + if (!this.focused) return + + var _self = this + + switch(e.keyCode) { + case 8: // backspace + if (!this.$input.is(document.activeElement)) break + this.lastInputValue = this.$input.val() + break + + case 37: // left arrow + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) + break + + case 38: // up arrow + upDown('prev') + break + + case 39: // right arrow + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) + break + + case 40: // down arrow + upDown('next') + break + + case 65: // a (to handle ctrl + a) + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break + this.activateAll() + e.preventDefault() + break + + case 9: // tab + case 13: // enter + + // We will handle creating tokens from autocomplete in autocomplete events + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break + + // We will handle creating tokens from typeahead in typeahead events + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break + + // Create token + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { + return this.createTokensFromInput(e, this.$input.data('edit')); + } + + // Edit token + if (e.keyCode === 13) { + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break + if (!_self.options.allowEditing) break + this.edit( this.$wrapper.find('.token.active') ) + } + } + + function leftRight(direction) { + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + direction += 'All' + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') + if (!$token.length) return + + _self.preventInputFocus = true + _self.preventDeactivation = true + + _self.activate( $token ) + e.preventDefault() + + } else { + _self[direction]( e.shiftKey ) + e.preventDefault() + } + } + + function upDown(direction) { + if (!e.shiftKey) return + + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') + if (!$token.length) return + + _self.activate( $token ) + } + + var opposite = direction === 'prev' ? 'next' : 'prev' + , position = direction === 'prev' ? 'first' : 'last' + + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { + _self.deactivate( $(this) ) + }) + + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) + e.preventDefault() + } + + this.lastKeyDown = e.keyCode + } + + , keypress: function(e) { + + // Comma + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { + if (this.$input.val()) { + this.createTokensFromInput(e) + } + return false; + } + } + + , keyup: function (e) { + this.preventInputFocus = false + + if (!this.focused) return + + switch(e.keyCode) { + case 8: // backspace + if (this.$input.is(document.activeElement)) { + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break + + this.preventDeactivation = true + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') + + if (!$prevToken.length) break + + this.activate( $prevToken ) + } else { + this.remove(e) + } + break + + case 46: // delete + this.remove(e, 'next') + break + } + this.lastKeyUp = e.keyCode + } + + , focus: function (e) { + this.focused = true + this.$wrapper.addClass('focus') + + if (this.$input.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + + if (this.options.showAutocompleteOnFocus) { + this.search() + } + } + } + + , blur: function (e) { + + this.focused = false + this.$wrapper.removeClass('focus') + + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + } + + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { + this.createTokensFromInput(e) + } + + this.preventDeactivation = false + this.preventCreateTokens = false + } + + , paste: function (e) { + var _self = this + + // Add tokens to existing ones + if (_self.options.allowPasting) { + setTimeout(function () { + _self.createTokensFromInput(e) + }, 1) + } + } + + , change: function (e) { + if ( e.initiator === 'tokenfield' ) return // Prevent loops + + this.setTokens( this.$element.val() ) + } + + , createTokensFromInput: function (e, focus) { + if (this.$input.val().length < this.options.minLength) + return // No input, simply return + + var tokensBefore = this.getTokensList() + this.setTokens( this.$input.val(), true ) + + if (tokensBefore == this.getTokensList() && this.$input.val().length) + return false // No tokens were added, do nothing (prevent form submit) + + if (this.$input.hasClass('tt-input')) { + // Typeahead acts weird when simply setting input value to empty, + // so we set the query to empty instead + this.$input.typeahead('val', '') + } else { + this.$input.val('') + } + + if (this.$input.data( 'edit' )) { + this.unedit(focus) + } + + return false // Prevent form being submitted + } + + , next: function (add) { + if (add) { + var $firstActiveToken = this.$wrapper.find('.active:first') + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $firstActiveToken ) + } + + var $lastActiveToken = this.$wrapper.find('.active:last') + , $nextToken = $lastActiveToken.nextAll('.token:first') + + if (!$nextToken.length) { + this.$input.focus() + return + } + + this.activate($nextToken, add) + } + + , prev: function (add) { + + if (add) { + var $lastActiveToken = this.$wrapper.find('.active:last') + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $lastActiveToken ) + } + + var $firstActiveToken = this.$wrapper.find('.active:first') + , $prevToken = $firstActiveToken.prevAll('.token:first') + + if (!$prevToken.length) { + $prevToken = this.$wrapper.find('.token:first') + } + + if (!$prevToken.length && !add) { + this.$input.focus() + return + } + + this.activate( $prevToken, add ) + } + + , activate: function ($token, add, multi, remember) { + + if (!$token) return + + if (typeof remember === 'undefined') var remember = true + + if (multi) var add = true + + this.$copyHelper.focus() + + if (!add) { + this.$wrapper.find('.active').removeClass('active') + if (remember) { + this.$firstActiveToken = $token + } else { + delete this.$firstActiveToken + } + } + + if (multi && this.$firstActiveToken) { + // Determine first active token and the current tokens indicies + // Account for the 1 hidden textarea by subtracting 1 from both + var i = this.$firstActiveToken.index() - 2 + , a = $token.index() - 2 + , _self = this + + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { + _self.activate( $(this), true ) + }) + } + + $token.addClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , activateAll: function() { + var _self = this + + this.$wrapper.find('.token').each( function (i) { + _self.activate($(this), i !== 0, false, false) + }) + } + + , deactivate: function($token) { + if (!$token) return + + $token.removeClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , toggle: function($token) { + if (!$token) return + + $token.toggleClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , edit: function ($token) { + if (!$token) return + + var attrs = $token.data('attrs') + + // Allow changing input value before editing + var options = { attrs: attrs, relatedTarget: $token.get(0) } + var editEvent = $.Event('tokenfield:edittoken', options) + this.$element.trigger( editEvent ) + + // Edit event can be cancelled if default is prevented + if (editEvent.isDefaultPrevented()) return + + $token.find('.token-label').text(attrs.value) + var tokenWidth = $token.outerWidth() + + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + + $token.replaceWith( $_input ) + + this.preventCreateTokens = true + + this.$input.val( attrs.value ) + .select() + .data( 'edit', true ) + .width( tokenWidth ) + + this.update(); + + // Indicate that token is now being edited, and is replaced with an input field in the DOM + this.$element.trigger($.Event('tokenfield:editedtoken', options )) + } + + , unedit: function (focus) { + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + $_input.appendTo( this.$wrapper ) + + this.$input.data('edit', false) + this.$mirror.text('') + + this.update() + + // Because moving the input element around in DOM + // will cause it to lose focus, we provide an option + // to re-focus the input after appending it to the wrapper + if (focus) { + var _self = this + setTimeout(function () { + _self.$input.focus() + }, 1) + } + } + + , remove: function (e, direction) { + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return + + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') + + if (e.type !== 'click') { + if (!direction) var direction = 'prev' + this[direction]() + + // Was it the first token? + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 + } + + // Prepare events and their options + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } + , removeEvent = $.Event('tokenfield:removetoken', options) + + this.$element.trigger(removeEvent); + + // Remove event can be intercepted and cancelled + if (removeEvent.isDefaultPrevented()) return + + var removedEvent = $.Event('tokenfield:removedtoken', options) + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) + + // Remove token from DOM + $token.remove() + + // Trigger events + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) + + // Focus, when necessary: + // When there are no more tokens, or if this was the first token + // and it was removed with backspace or it was clicked on + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() + + // Adjust input width + this.$input.css('width', this.options.minWidth + 'px') + this.update() + + // Cancel original event handlers + e.preventDefault() + e.stopPropagation() + } + + /** + * Update tokenfield dimensions + */ + , update: function (e) { + var value = this.$input.val() + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) + , inputPadding = inputPaddingLeft + inputPaddingRight + + if (this.$input.data('edit')) { + + if (!value) { + value = this.$input.prop("placeholder") + } + if (value === this.$mirror.text()) return + + this.$mirror.text(value) + + var mirrorWidth = this.$mirror.width() + 10; + if ( mirrorWidth > this.$wrapper.width() ) { + return this.$input.width( this.$wrapper.width() ) + } + + this.$input.width( mirrorWidth ) + } + else { + var w = (this.textDirection === 'rtl') + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; + // + // some usecases pre-render widget before attaching to DOM, + // dimensions returned by jquery will be NaN -> we default to 100% + // so placeholder won't be cut off. + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); + } + } + + , focusInput: function (e) { + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return + // Focus only after the current call stack has cleared, + // otherwise has no effect. + // Reason: mousedown is too early - input will lose focus + // after mousedown. However, since the input may be moved + // in DOM, there may be no click or mouseup event triggered. + var _self = this + setTimeout(function() { + _self.$input.focus() + }, 0) + } + + , search: function () { + if ( this.$input.data('ui-autocomplete') ) { + this.$input.autocomplete('search') + } + } + + , disable: function () { + this.setProperty('disabled', true); + } + + , enable: function () { + this.setProperty('disabled', false); + } + + , readonly: function () { + this.setProperty('readonly', true); + } + + , writeable: function () { + this.setProperty('readonly', false); + } + + , setProperty: function(property, value) { + this['_' + property] = value; + this.$input.prop(property, value); + this.$element.prop(property, value); + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); + } + + , destroy: function() { + // Set field value + this.$element.val( this.getTokensList() ); + // Restore styles and properties + this.$element.css( this.$element.data('original-styles') ); + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); + + // Re-route tokenfield label to original input + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$element.prop('id') ) + } + + // Move original element outside of tokenfield wrapper + this.$element.insertBefore( this.$wrapper ); + + // Remove tokenfield-related data + this.$element.removeData('original-styles') + .removeData('original-tabindex') + .removeData('bs.tokenfield'); + + // Remove tokenfield from DOM + this.$wrapper.remove(); + this.$mirror.remove(); + + var $_element = this.$element; + + return $_element; + } + + } + + + /* TOKENFIELD PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.tokenfield + + $.fn.tokenfield = function (option, param) { + var value + , args = [] + + Array.prototype.push.apply( args, arguments ); + + var elements = this.each(function () { + var $this = $(this) + , data = $this.data('bs.tokenfield') + , options = typeof option == 'object' && option + + if (typeof option === 'string' && data && data[option]) { + args.shift() + value = data[option].apply(data, args) + } else { + if (!data && typeof option !== 'string' && !param) { + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) + $this.trigger('tokenfield:initialize') + } + } + }) + + return typeof value !== 'undefined' ? value : elements; + } + + $.fn.tokenfield.defaults = { + minWidth: 60, + minLength: 0, + html: true, + allowEditing: true, + allowPasting: true, + limit: 0, + autocomplete: {}, + typeahead: {}, + showAutocompleteOnFocus: false, + createTokensOnBlur: false, + delimiter: ',', + beautify: true, + inputType: 'text' + } + + $.fn.tokenfield.Constructor = Tokenfield + + + /* TOKENFIELD NO CONFLICT + * ================== */ + + $.fn.tokenfield.noConflict = function () { + $.fn.tokenfield = old + return this + } + + return Tokenfield; + +})); diff --git a/plugins/fb_app/public/javascripts/fb_app.js b/plugins/fb_app/public/javascripts/fb_app.js new file mode 100644 index 0000000..83a43c5 --- /dev/null +++ b/plugins/fb_app/public/javascripts/fb_app.js @@ -0,0 +1,312 @@ +fb_app = { + current_url: '', + + locales: { + + }, + + config: { + url_prefix: '', + save_auth_url: '', + show_login_url: '', + + init: function() { + + }, + + }, + + timeline: { + appId: '', + app_scope: 'publish_actions', + + loading: function() { + jQuery('#fb-app-connect-status').empty().addClass('loading').height(150) + }, + + connect: function() { + this.loading(); + fb_app.fb.scope = this.app_scope + fb_app.fb.connect(function (response) { + fb_app.auth.receive(response) + }); + }, + + disconnect: function() { + // 'not_authorized' is used to disconnect from facebook + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html( + fb_app.locales.confirm_disconnect + ) + jQuery('#fb-app-modal-wrap .modal-button-no') + .html(fb_app.locales.cancel_button) + .attr('onClick', 'noosfero.modal.close(); return false') + jQuery('#fb-app-modal-wrap .modal-button-yes') + .html(fb_app.locales.confirm_disconnect_button) + .attr('onClick', 'fb_app.timeline.disconnect_confirmed();noosfero.modal.close(); return false') + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html()) + }, + + disconnect_confirmed: function() { + this.loading(); + fb_app.auth.receive({status: 'not_authorized'}) + }, + + connect_to_another: function() { + this.disconnect(); + fb_app.fb.connect_to_another(this.connect) + }, + }, + + page_tab: { + appId: '', + nextUrl: '', + + init: function() { + FB.Canvas.scrollTo(0,140); + // While Braulio doesnt make the catalog-options-bar work in a product page, I'll hide it. User will have to go back to the catalog to see this bar, provisorily... + jQuery('#product-page').prev('form').hide(); + }, + + config: { + + init: function() { + this.change_type($('select#page_tab_config_type')) + + }, + + edit: function(button) { + var page_tab = button.parents('.page-tab') + page_tab.find('form').toggle(400) + }, + + remove: function(button, url) { + var page_tab = button.parents('.page-tab') + var name = page_tab.find('#page_tab_name').val() + //jQuery('#fb-app-modal-catalog-name').text(name) + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html( + fb_app.locales.confirm_removal + ) + jQuery('#fb-app-modal-wrap .modal-button-no') + .html(fb_app.locales.cancel_button) + .attr('onClick', 'noosfero.modal.close(); return false') + jQuery('#fb-app-modal-wrap .modal-button-yes') + .html(fb_app.locales.confirm_removal_button) + .attr('onClick', 'fb_app.page_tab.config.remove_confirmed(this);noosfero.modal.close(); return false') + .attr('target_url',url) + .attr('target_id','#'+page_tab.attr('id')) + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html()) + }, + + remove_confirmed: function(el) { + el = jQuery(el) + jQuery.post(el.attr('target_url'), function() { + var page_tab = jQuery(el.attr('target_id')) + page_tab.remove() + }) + }, + + close: function(pageId) { + noosfero.modal.close() + jQuery('#content').html('').addClass('loading') + fb_app.fb.redirect_to_tab(pageId, fb_app.page_tab.appId) + }, + + validate: function(form) { + for (var i=0; tinymce.editors[i]; i++) { + var editor = tinymce.editors[i] + var textarea = editor.getElement() + textarea.value = editor.getContent() + } + + if (form.find('#page_tab_title').val().trim()=='') { + noosfero.modal.html('
'+fb_app.locales.error_empty_title+'
') + return false + } else { + var selected_type = form.find('#page_tab_config_type').val() + var sub_option = form.find('.config-type-'+selected_type+' input') + if (sub_option.length > 0 && sub_option.val().trim()=='') { + noosfero.modal.html('
'+fb_app.locales.error_empty_settings+'
') + return false + } + } + return true + }, + + add: function (form) { + if (!this.validate(form)) + return false + // this checks if the user is using FB as a page and offer a switch + FB.login(function(response) { + if (response.status != 'connected') return + var nextUrl = fb_app.page_tab.nextUrl + '?' + form.serialize() + window.location.href = fb_app.fb.add_tab_url(fb_app.page_tab.appId, nextUrl) + }) + return false + }, + + save: function(form) { + if (!this.validate(form)) + return false + jQuery(form).ajaxSubmit({ + dataType: 'script', + }) + return false + }, + + change_type: function(select) { + select = jQuery(select) + var page_tab = select.parents('.page-tab') + var config_selector = '.config-type-'+select.val() + var config = page_tab.find(config_selector) + var to_show = config + var to_hide = page_tab.find('.config-type:not('+config_selector+')') + + to_show.show(). + find('input').prop('disabled', false) + to_show.find('.tokenfield').removeClass('disabled') + to_hide.hide(). + find('input').prop('disabled', true) + }, + + profile: { + + onchange: function(input) { + if (input.val()) + input.removeAttr('placeholder') + else + input.attr('placeholder', input.attr('data-placeholder')) + }, + }, + }, + + }, + + auth: { + status: 'not_authorized', + + load: function (html) { + jQuery('#fb-app-settings').html(html) + }, + loadLogin: function (html) { + if (this.status == 'not_authorized') + jQuery('#fb-app-connect').html(html).removeClass('loading') + else + jQuery('#fb-app-login').html(html) + }, + + receive: function(response) { + fb_app.fb.authResponse = response + fb_app.auth.save(response) + jQuery('html,body').animate({ scrollTop: jQuery('#fb-app-settings').offset().top-100 }, 400) + }, + + transformParams: function(response) { + var authResponse = response.authResponse + if (!authResponse) + return {auth: {status: response.status}} + else + return { + auth: { + status: response.status, + access_token: authResponse.accessToken, + expires_in: authResponse.expiresIn, + signed_request: authResponse.signedRequest, + provider_user_id: authResponse.userID, + } + } + }, + + showLogin: function(response) { + jQuery.get(fb_app.config.show_login_url, this.transformParams(response), this.loadLogin) + }, + + save: function(response) { + jQuery.post(fb_app.config.save_auth_url, this.transformParams(response), this.load) + }, + }, + + + // interface to facebook's SDK + fb: { + appId: '', + scope: '', + inited: false, + initCode: null, + + prepareAsyncInit: function(appId, asyncInitCode) { + this.id = appId + this.initCode = asyncInitCode + + window.fbAsyncInit = function() { + FB.init({ + appId: appId, + cookie: true, + xfbml: true, + status: true, + }) + + // automatic iframe's resize + // FIXME: move to page tab embed code + fb_app.fb.size_change() + jQuery(document).on('DOMNodeInserted', fb_app.fb.size_change) + + if (asyncInitCode) + jQuery.globalEval(asyncInitCode) + + fb_app.fb.inited = true + } + }, + + init: function() { + // the SDK is loaded on views/fb_app_plugin/_load.html.slim and then call window.fbAsyncInit + }, + + size_change: function() { + FB.Canvas.setSize({height: jQuery('body').height()+100}) + }, + + redirect_to_tab: function(pageId, appId) { + window.location.href = 'https://facebook.com/' + pageId + '?sk=app_' + appId + }, + + add_tab_url: function (appId, nextUrl) { + return 'https://www.facebook.com/dialog/pagetab?' + jQuery.param({app_id: appId, next: nextUrl}) + }, + + connect: function(callback) { + FB.login(function(response) { + if (callback) callback(response) + }, {scope: fb_app.fb.scope}) + }, + + connect_to_another: function(callback) { + this.logout(this.connect(callback)) + }, + + logout: function(callback) { + // this checks if the user is using FB as a page and offer a switch + FB.login(function(response) { + FB.logout(function(response) { + if (callback) callback(response) + }) + }) + }, + + // not to be used + delete: function(callback) { + FB.api("/me/permissions", "DELETE", function(response) { + if (callback) callback(response) + }) + }, + + checkLoginStatus: function() { + FB.getLoginStatus(function(response) { + // don't do nothing, this is just to fetch auth after init + }) + }, + + }, + +} + + diff --git a/plugins/fb_app/public/style.scss b/plugins/fb_app/public/style.scss new file mode 120000 index 0000000..49c35bf --- /dev/null +++ b/plugins/fb_app/public/style.scss @@ -0,0 +1 @@ +stylesheets/style.scss \ No newline at end of file diff --git a/plugins/fb_app/public/stylesheets/_base.scss b/plugins/fb_app/public/stylesheets/_base.scss new file mode 100644 index 0000000..abb433b --- /dev/null +++ b/plugins/fb_app/public/stylesheets/_base.scss @@ -0,0 +1,214 @@ +/* use with @extend, CSS clear bugfix */ +.clean { + clear: both; +} +.container-clean { + overflow: hidden; + display: inline-block; /* Necessary to trigger "hasLayout" in IE */ + display: block; /* Sets element back to block */ +} + +/* layout base parameters */ +$modules: 12; +$base: 8px; +$wireframe: 1040px; + +/* heights should only use multiples of this */ +$height: $base; + +/* base measurements */ +$intercolumn: 2*$base; +$module: $wireframe/$modules - $intercolumn; + +/* widths should only use one of these */ +$module01: 01*$module + 00*$intercolumn; +$module02: 02*$module + 01*$intercolumn; +$module03: 03*$module + 02*$intercolumn; +$module04: 04*$module + 03*$intercolumn; +$module05: 05*$module + 04*$intercolumn; +$module06: 06*$module + 05*$intercolumn; +$module07: 07*$module + 06*$intercolumn; +$module08: 08*$module + 07*$intercolumn; +$module09: 09*$module + 08*$intercolumn; +$module09: 09*$module + 08*$intercolumn; +$module10: 10*$module + 09*$intercolumn; +$module11: 11*$module + 10*$intercolumn; +$module12: 12*$module + 11*$intercolumn; +$module01p: percentage($module01/$wireframe); +$module02p: percentage($module02/$wireframe); +$module03p: percentage($module03/$wireframe); +$module04p: percentage($module04/$wireframe); +$module05p: percentage($module05/$wireframe); +$module06p: percentage($module06/$wireframe); +$module07p: percentage($module07/$wireframe); +$module08p: percentage($module08/$wireframe); +$module09p: percentage($module09/$wireframe); +$module10p: percentage($module10/$wireframe); +$module11p: percentage($module11/$wireframe); +$module12p: percentage($module12/$wireframe); + +/* paddings and margins should only use one of these + Ps. 1: disccount the borders size from padding, as borders uses padding's space. + Ps. 2: because of W3C's content-box default box sizing, padding sums to width size. If your + box doesn't have a padding, then sum $intercolumn to the width. + */ +$margin: $intercolumn; +$half-margin: $margin/2; +$padding: $intercolumn/2; +$half-padding: $padding/2; +$marginp: percentage($margin/$wireframe); +$half-marginp: percentage($half-margin/$wireframe); +$paddingp: percentage($padding/$wireframe); +$half-paddingp: percentage($half-padding/$wireframe); + +$wireframe-padding: 5*$padding; + +/* use for borders */ +$border: 1px; +$border-radius: 5px; + +/* use for text shadows */ +$shadow: 2px; + +/* Colors */ + +$border-action-button: #F4A439; +$bg-action-button: #FBCA47; +$bg-selection-button: white; + +/* Fonts */ + +/* Paragraphs Styles (use with @extend) */ + +.pstyle-none { + font-size: 12px; +} +.pstyle-basic { + font-size: 16px; +} +.pstyle-button { + font-size: 16px; +} +.pstyle-button-small { + font-size: 13px; +} +.pstyle-title { + font-size: 72px; +} +.pstyle-h1 { + font-size: 34px; +} +.pstyle-h2 { + font-size: 26px; +} +.pstyle-h3 { + font-size: 21px; +} +.pstyle-h4 { + font-size: 16px; +} +.pstyle-h5 { + font-size: 13px; +} +.pstyle-title-section { + font-size: 92px; +} +.pstyle-field { + font-size: 13px; +} +.pstyle-menu-big-selected { + font-size: 21px; +} +.pstyle-menu-big-unselected { + font-size: 21px; +} +.pstyle-menu-medium-selected { + font-size: 16px; +} +.pstyle-menu-medium-unselected { + font-size: 16px; +} +.pstyle-menu-small-selected { + font-size: 13px; +} +.pstyle-menu-small-unselected { + font-size: 13px; +} +.pstyle-tp4 { + font-size: 42px; +} +.pstyle-tp3 { + font-size: 34px; +} +.pstyle-tp2 { + font-size: 26px; +} +.pstyle-tp1 { + font-size: 21px; +} +.pstyle-tm1 { + font-size: 13px; +} +.pstyle-tm2 { + font-size: 10px; +} +.subtitle { + @extend .pstyle-tm2; +} + +/* Images */ + +$profile-thumb-size: 4*$base; +$profile-portrait-size: 10*$base; + +/* profile-image that can be centered and resized with aspect ratio */ +.profile-image { + display: inline-block; + + &.thumb { + width: $profile-thumb-size; + height: $profile-thumb-size; + } + &.portrait { + width: $profile-portrait-size; + height: $profile-portrait-size; + } + + /* do not put padding in this as background size will consider it. */ + .inner { + display: block; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + background-size: contain; /* css3 enabled */ + } +} + +/* Buttons */ + +.action-button { + display: inline-block; + padding: $half-padding $padding; + height: auto; + width: auto; + //&:visited, &:active, &:hover { color: white; } + background: $bg-action-button; + border: $border solid $border-action-button; + cursor: pointer; + color: black; + font-weight: bold; + line-height: 2*$height; + text-align: center; + text-decoration: none; + text-transform: uppercase; + text-shadow: none; + border-radius: $border-radius; +} + +.selection-button { + @extend .action-button; + background: $bg-selection-button; +} + diff --git a/plugins/fb_app/public/stylesheets/bootstrap-tokenfield.css b/plugins/fb_app/public/stylesheets/bootstrap-tokenfield.css new file mode 100644 index 0000000..268fb84 --- /dev/null +++ b/plugins/fb_app/public/stylesheets/bootstrap-tokenfield.css @@ -0,0 +1,209 @@ +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */ +@-webkit-keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +@-moz-keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +@keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +.tokenfield { + height: auto; + min-height: 34px; + padding-bottom: 0px; +} +.tokenfield.focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); +} +.tokenfield .token { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + display: inline-block; + border: 1px solid #d9d9d9; + background-color: #ededed; + white-space: nowrap; + margin: -1px 5px 5px 0; + height: 22px; + vertical-align: top; + cursor: default; +} +.tokenfield .token:hover { + border-color: #b9b9b9; +} +.tokenfield .token.active { + border-color: #52a8ec; + border-color: rgba(82, 168, 236, 0.8); +} +.tokenfield .token.duplicate { + border-color: #ebccd1; + -webkit-animation-name: blink; + animation-name: blink; + -webkit-animation-duration: 0.1s; + animation-duration: 0.1s; + -webkit-animation-direction: normal; + animation-direction: normal; + -webkit-animation-timing-function: ease; + animation-timing-function: ease; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} +.tokenfield .token.invalid { + background: none; + border: 1px solid transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + border-bottom: 1px dotted #d9534f; +} +.tokenfield .token.invalid.active { + background: #ededed; + border: 1px solid #ededed; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.tokenfield .token .token-label { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 4px; + vertical-align: top; +} +.tokenfield .token .close { + font-family: Arial; + display: inline-block; + line-height: 100%; + font-size: 1.1em; + line-height: 1.49em; + margin-left: 5px; + float: none; + height: 100%; + vertical-align: top; + padding-right: 4px; +} +.tokenfield .token-input { + background: none; + width: 60px; + min-width: 60px; + border: 0; + height: 20px; + padding: 0; + margin-bottom: 6px; + -webkit-box-shadow: none; + box-shadow: none; +} +.tokenfield .token-input:focus { + border-color: transparent; + outline: 0; + /* IE6-9 */ + -webkit-box-shadow: none; + box-shadow: none; +} +.tokenfield.disabled { + cursor: not-allowed; + background-color: #eeeeee; +} +.tokenfield.disabled .token-input { + cursor: not-allowed; +} +.tokenfield.disabled .token:hover { + cursor: not-allowed; + border-color: #d9d9d9; +} +.tokenfield.disabled .token:hover .close { + cursor: not-allowed; + opacity: 0.2; + filter: alpha(opacity=20); +} +.has-warning .tokenfield.focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; +} +.has-error .tokenfield.focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} +.has-success .tokenfield.focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} +.tokenfield.input-sm, +.input-group-sm .tokenfield { + min-height: 30px; + padding-bottom: 0px; +} +.input-group-sm .token, +.tokenfield.input-sm .token { + height: 20px; + margin-bottom: 4px; +} +.input-group-sm .token-input, +.tokenfield.input-sm .token-input { + height: 18px; + margin-bottom: 5px; +} +.tokenfield.input-lg, +.input-group-lg .tokenfield { + min-height: 45px; + padding-bottom: 4px; +} +.input-group-lg .token, +.tokenfield.input-lg .token { + height: 25px; +} +.input-group-lg .token-label, +.tokenfield.input-lg .token-label { + line-height: 23px; +} +.input-group-lg .token .close, +.tokenfield.input-lg .token .close { + line-height: 1.3em; +} +.input-group-lg .token-input, +.tokenfield.input-lg .token-input { + height: 23px; + line-height: 23px; + margin-bottom: 6px; + vertical-align: top; +} +.tokenfield.rtl { + direction: rtl; + text-align: right; +} +.tokenfield.rtl .token { + margin: -1px 0 5px 5px; +} +.tokenfield.rtl .token .token-label { + padding-left: 0px; + padding-right: 4px; +} diff --git a/plugins/fb_app/public/stylesheets/style.scss b/plugins/fb_app/public/stylesheets/style.scss new file mode 100644 index 0000000..77128bc --- /dev/null +++ b/plugins/fb_app/public/stylesheets/style.scss @@ -0,0 +1,238 @@ +@import 'base'; + +body.controller-fb_app_plugin_page_tab { + + /* Catalog Tab in Facebook */ + background: #fff; + + #top-bar, #theme-footer, #product-category, #product-page .button-bar { + display: none; + } + .navbar-static-top { + border-radius: 4px; + } + #wrap-1 { + box-shadow: none; + } + .container { + width: 100%; + } + #content .no-boxes { + padding: 0; + width: 100%; + } + #content-inner { + padding-top: 0; + } + #content h1 { + margin: 0; + } + #product-list { + margin: 0 -10px; + } + #page-tab-subtitle { + margin-bottom: 15px; + background-color: rgb(241, 255, 107); + border: 1px solid #ccc; + border-top: none; + border-radius: 0px 0px 6px 6px; + font-style: italic; + padding: 10px 10px 0px; + } + #page-tab-subtitle p { + margin-bottom: 10px; + } + #product-owner { + display:block; + font-size: 120%; + font-weight: bold; + clear: both; + } + #product-list li.product { + width: 190px; + padding: 10px; + } + #product-list .product-big { + width: 160px; + } + #product-list .product-image-link { + height: 170px; + } + #product-list .expand-box { + width: 162px; + } + #theme-footer { + border: none; + } + #page-tab-footer { + font-size: 11px; + border-top: 3px solid rgb(241, 255, 107); + margin-top: 70px; + padding-top: 5px; + } + #page-tab-footer1 { + background-size: 50px; + padding-left: 50px; + } + + /* End of Catalog Tab in Facebook */ + + #profile-title, + #profile-header, + #profile-theme-header, + #profile-footer, + #theme-header { + display: none; + } + .product-catalog-ctrl { + float: right; + margin-left: 3px; + } + #product-catalog-actions { + text-align: right; + } + #manage-fb-store-ctrl { + margin-bottom: 15px; + } + .modal-content { + #fb-app-page-tab-admin { + height: 400px; + } + } + +} + +body.controller-fb_app_plugin_page_tab, +body.controller-fb_app_plugin_myprofile { + + input.small-loading { + background: transparent url(/images/loading-small.gif) no-repeat scroll right center; + } + + .loading { + background: white url(/plugins/fb_app/images/loading.gif) no-repeat center center; + width: 80%; + height: 300px; + margin: auto; + } +} + +/* control panel - general */ +#fb-app-modal-wrap { + display: none; +} +#fb-app-error, #fb-app-modal { + padding: 70px 30px; + font-size: 120%; +} +#fb-app-error { + color: #E44444; + font-style: italic; +} +.controller-profile_editor a.control-panel-fb-app { + background-image: url(/plugins/fb_app/images/control-panel.png); +} +#fb-app-intro { + margin: 0 40px 20px 40px; +} +#fb-app-intro-text { + border:2px #666 solid; + padding: 10px; + border-radius: 8px; +} +#fb-app-connect-status { + background-color: #eee; + border: 1px solid #ccc; + margin: 30px 0; + padding: 30px; +} +.fb-app-connection-button { + margin-top: 15px; +} +#fb-app-auth { + text-align: center; + min-height: 60px; +} +#fb-connected { + font-size: 36px; + margin: 0 30px; + color: #99f; +} +#fb-app-wrapper { + padding: 40px; + font-size: 20px; +} +#fb-app-wrapper h1 { + font-size: 39px; + margin-bottom: 50px; +} + +/* Control panel - catalog settings */ +#page-tab-new { + margin-top: 30px; +} +#page-tab-new h3 { + border-top: 2px solid #ddd; + padding-top: 10px; +} +#page-tab-new h3, .edit-page-tab { + display: none; +} +.edit-page-tab { + background-color: #eee; + padding: 15px; + border-radius: 8px; +} +.edit-tab-button { + float: right; + margin-left: 10px; +} +#fb-app-timeline, #fb-app-catalogs { + border: 1px solid #999; + border-radius: 8px; + padding: 10px; +} +#content #fb-app-catalogs h3 { + font-size: 120%; + color: inherit; + margin-top: 20px; +} +#fb-app-catalogs label { + margin-top: 20px; +} +.fb-app-submit-page-tab-options { + margin-top: 20px; +} +.fb-app-final-back-button { + margin-top: 70px; +} +.tokenfield .token { + height: auto !important; +} + +@media (min-width: 768px) { + #noosfero-identity { + float: left; + } + #fb-connected { + font-size: 36px; + float:left; + margin: 0 30px; + color: #99f; + } + #fb-identity { + float: left; + } + #fb-app-timeline, #fb-app-catalogs { + width: 50%-$marginp; + float: left; + } + #fb-app-settings { + overflow: hidden; + padding-bottom: 400px; + } + #fb-app-catalogs { + margin-right: $marginp; + } +} + diff --git a/plugins/fb_app/public/stylesheets/tokenfield-typeahead.css b/plugins/fb_app/public/stylesheets/tokenfield-typeahead.css new file mode 100644 index 0000000..53038da --- /dev/null +++ b/plugins/fb_app/public/stylesheets/tokenfield-typeahead.css @@ -0,0 +1,141 @@ +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */ +/* General Typeahead styling, from http://jsfiddle.net/ragulka/Dy9au/1/ */ +.twitter-typeahead { + width: 100%; + position: relative; + vertical-align: top; +} +.twitter-typeahead .tt-input, +.twitter-typeahead .tt-hint { + margin: 0; + width: 100%; + vertical-align: middle; + background-color: #ffffff; +} +.twitter-typeahead .tt-hint { + color: #999999; + z-index: 1; + border: 1px solid transparent; +} +.twitter-typeahead .tt-input { + color: #555555; + z-index: 2; +} +.twitter-typeahead .tt-input, +.twitter-typeahead .tt-hint { + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.428571429; +} +.twitter-typeahead .input-sm.tt-input, +.twitter-typeahead .hint-sm.tt-hint { + border-radius: 3px; +} +.twitter-typeahead .input-lg.tt-input, +.twitter-typeahead .hint-lg.tt-hint { + border-radius: 6px; +} +.input-group .twitter-typeahead:first-child .tt-input, +.input-group .twitter-typeahead:first-child .tt-hint { + border-radius: 4px 0 0 4px !important; +} +.input-group .twitter-typeahead:last-child .tt-input, +.input-group .twitter-typeahead:last-child .tt-hint { + border-radius: 0 4px 4px 0 !important; +} +.input-group.input-group-sm .twitter-typeahead:first-child .tt-input, +.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint { + border-radius: 3px 0 0 3px !important; +} +.input-group.input-group-sm .twitter-typeahead:last-child .tt-input, +.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint { + border-radius: 0 3px 3px 0 !important; +} +.input-sm.tt-input, +.hint-sm.tt-hint, +.input-group.input-group-sm .tt-input, +.input-group.input-group-sm .tt-hint { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-group.input-group-lg .twitter-typeahead:first-child .tt-input, +.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint { + border-radius: 6px 0 0 6px !important; +} +.input-group.input-group-lg .twitter-typeahead:last-child .tt-input, +.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint { + border-radius: 0 6px 6px 0 !important; +} +.input-lg.tt-input, +.hint-lg.tt-hint, +.input-group.input-group-lg .tt-input, +.input-group.input-group-lg .tt-hint { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; +} +.tt-dropdown-menu { + width: 100%; + min-width: 160px; + margin-top: 2px; + padding: 5px 0; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + *border-right-width: 2px; + *border-bottom-width: 2px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} +.tt-suggestion { + display: block; + padding: 3px 20px; +} +.tt-suggestion.tt-cursor { + color: #262626; + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); +} +.tt-suggestion.tt-cursor a { + color: #ffffff; +} +.tt-suggestion p { + margin: 0; +} +/* Tokenfield-specific Typeahead styling */ +.tokenfield .twitter-typeahead { + width: auto; +} +.tokenfield .twitter-typeahead .tt-hint { + padding: 0; + height: 20px; +} +.tokenfield.input-sm .twitter-typeahead .tt-input, +.tokenfield.input-sm .twitter-typeahead .tt-hint { + height: 18px; + font-size: 12px; + line-height: 1.5; +} +.tokenfield.input-lg .twitter-typeahead .tt-input, +.tokenfield.input-lg .twitter-typeahead .tt-hint { + height: 23px; + font-size: 18px; + line-height: 1.33; +} +.tokenfield .twitter-typeahead .tt-suggestions { + font-size: 14px; +} diff --git a/plugins/fb_app/views/fb_app_plugin/_load.html.slim b/plugins/fb_app/views/fb_app_plugin/_load.html.slim new file mode 100644 index 0000000..6a16142 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin/_load.html.slim @@ -0,0 +1,39 @@ += content_for :head do + = javascript_include_tag 'typeahead.bundle.js' + = stylesheet_link_tag 'typeahead' + = stylesheet_link_tag 'plugins/fb_app/stylesheets/bootstrap-tokenfield.css' + = stylesheet_link_tag 'plugins/fb_app/stylesheets/tokenfield-typeahead.css' + = javascript_include_tag 'plugins/fb_app/javascripts/bootstrap-tokenfield.js' + = javascript_include_tag 'plugins/open_graph/javascripts/open_graph.js' + +- callback = '' unless defined? callback + +#fb-root +#fb-app-modal-wrap style="display:none" + #fb-app-modal + #fb-app-modal-intro + = button_to_function 'cancel', '', "ff()", class: 'modal-button-no' + = button_to_function 'ok', '', "ff()", class: 'modal-button-yes' + +javascript: + // Adding locales: + fb_app.locales.error_empty_title = #{t('fb_app_plugin.views.myprofile.error.empty_title').to_json} + fb_app.locales.error_empty_settings = #{t('fb_app_plugin.views.myprofile.error.empty_settings').to_json} + fb_app.locales.cancel_button = #{t('fb_app_plugin.views.myprofile.catalogs.cancel_button').to_json} + fb_app.locales.confirm_removal = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal').to_json} + fb_app.locales.confirm_removal_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal_button').to_json} + fb_app.locales.confirm_disconnect = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect').to_json} + fb_app.locales.confirm_disconnect_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect_button').to_json} + + // General settings: + fb_app.current_url = #{url_for(params).to_json}; + fb_app.base_url = 'https://#{environment.default_hostname}/plugin/fb_app'; + + fb_app.page_tab.appId = #{FbAppPlugin.page_tab_app_credentials[:id].to_json}, + fb_app.timeline.appId = #{FbAppPlugin.timeline_app_credentials[:id].to_json}, + fb_app.page_tab.nextUrl = #{url_for(protocol: 'https', only_path: false).to_json} + + fb_app.fb.prepareAsyncInit(fb_app.timeline.appId, #{callback.to_json}); + fb_app.fb.init(); +/ must come after window.fbAsyncInit is defined += javascript_include_tag "https://connect.facebook.net/en_US/all.js" diff --git a/plugins/fb_app/views/fb_app_plugin/index.html.erb b/plugins/fb_app/views/fb_app_plugin/index.html.erb new file mode 100644 index 0000000..f4081a6 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin/index.html.erb @@ -0,0 +1,4 @@ +
+
+ +<%# render 'load' %> diff --git a/plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim b/plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim new file mode 100644 index 0000000..b1a61e4 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim @@ -0,0 +1,8 @@ +html + head + = noosfero_javascript + = javascript_include_tag 'fb_app' + = noosfero_stylesheets + = h stylesheet_link_tag(jquery_ui_theme_stylesheet_path) + body + = yield diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/_auth.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/_auth.html.slim new file mode 100644 index 0000000..9f64ddb --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/_auth.html.slim @@ -0,0 +1,14 @@ +- if @auth.connected? + #noosfero-identity + = profile_image profile + = profile.name + span#fb-connected.fa.fa-arrows-h + #fb-identity + = render 'identity', auth: @auth + + = button_to_function 'close', t('fb_app_plugin.views.myprofile.disconnect'), 'fb_app.timeline.disconnect()', class:'fb-app-connection-button' + +- elsif @auth.not_authorized? + = button_to_function 'login', t('fb_app_plugin.views.myprofile.connect'), 'fb_app.timeline.connect()', size: '', option: 'primary', class:'fb-app-connection-button' +- elsif @auth.expired? + = button_to_function 'login', t('fb_app_plugin.views.myprofile.reconnect'), 'fb_app.timeline.reconnect()', class:'fb-app-connection-button' diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/_catalogs.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/_catalogs.html.slim new file mode 100644 index 0000000..d5427ab --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/_catalogs.html.slim @@ -0,0 +1,11 @@ +h2 + = t'fb_app_plugin.views.myprofile.catalogs.heading' + = render 'catalogs_help' rescue nil + +- profile.fb_app_page_tabs.each do |page_tab| + = render 'fb_app_plugin_page_tab/config', page_tab: page_tab + +#new-catalog + = render 'fb_app_plugin_page_tab/config', page_tab: profile.fb_app_page_tabs.build(owner_profile: profile) + += render file: 'shared/tiny_mce', locals: {mode: 'simple'} diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/_identity.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/_identity.html.slim new file mode 100644 index 0000000..6accb95 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/_identity.html.slim @@ -0,0 +1,7 @@ +.fb-app-identity + - picture = auth.fb_user.picture + / fb_graph version 1 compatibility + - url = if picture.respond_to? :url then picture.url else picture end + = image_tag url + = auth.fb_user.name + diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/_load.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/_load.html.slim new file mode 100644 index 0000000..b2ab8b4 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/_load.html.slim @@ -0,0 +1,7 @@ += render 'fb_app_plugin/load', callback: 'fb_app.fb.checkLoginStatus()' + +javascript: + fb_app.config.url_prefix = #{url_for(action: :index).to_json} + fb_app.config.save_auth_url = #{url_for(action: :save_auth).to_json} + fb_app.config.show_login_url = #{url_for(action: :show_login).to_json} + diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/_settings.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/_settings.html.slim new file mode 100644 index 0000000..07d95af --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/_settings.html.slim @@ -0,0 +1,28 @@ +h1= t'fb_app_plugin.lib.plugin.name' += button :back, _('Back to control panel'), controller: 'profile_editor' + +#fb-app-connect-status + = render 'intro' rescue nil if @auth.not_authorized? + #fb-app-auth + = render 'auth' + +- if @auth.connected? or Rails.env.development? + #fb-app-catalogs + = render 'catalogs' + #fb-app-timeline + - if profile.person? + h2= t'fb_app_plugin.views.myprofile.timeline.heading' + + - unless FbAppPlugin.test_user? user + h3= t'fb_app_plugin.views.myprofile.timeline.explanation_title' + p= t'fb_app_plugin.views.myprofile.timeline.explanation_text' + - else + #track-form + = render 'track_form', context: :fb_app + - else + = t'fb_app_plugin.views.myprofile.timeline.organization_redirect', + type: t("fb_app_plugin.views.myprofile.timeline.organization_from_#{profile.class.name.underscore}"), + redirect_link: link_to(t('fb_app_plugin.views.myprofile.timeline.redirect_link'), host: FbAppPlugin.config[:app][:domain], profile: user.identifier, controller: :fb_app_plugin_myprofile) +.clean + += button :back, _('Back to control panel'), {controller: 'profile_editor'}, class: 'fb-app-final-back-button' diff --git a/plugins/fb_app/views/fb_app_plugin_myprofile/index.html.slim b/plugins/fb_app/views/fb_app_plugin_myprofile/index.html.slim new file mode 100644 index 0000000..c4fe15d --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_myprofile/index.html.slim @@ -0,0 +1,6 @@ += render 'load' +javascript: + fb_app.auth.status = #{(@auth.status rescue FbAppPlugin::Auth::Status::NotAuthorized).to_json} + +#fb-app-settings + = render 'settings' diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_config.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_config.html.slim new file mode 100644 index 0000000..f84a2e4 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_config.html.slim @@ -0,0 +1,13 @@ +.page-tab id="page-tab-#{page_tab.id || 'new'}" + - if page_tab.page_id + h3 + = page_tab.title + = button_to_function_without_text 'edit', t('fb_app_plugin.views.myprofile.catalogs.edit_button') % {catalog_title: page_tab.title}, + "fb_app.page_tab.config.edit($(this))", class: 'edit-tab-button' + = button_to_function_without_text 'remove', t('fb_app_plugin.views.myprofile.catalogs.remove_button') % {catalog_title: page_tab.title}, + "fb_app.page_tab.config.remove($(this), '#{url_for(controller: :fb_app_plugin_page_tab, action: :destroy, id: page_tab.id)}')", class: 'edit-tab-button' + = button_without_text 'eyes', t('fb_app_plugin.views.myprofile.catalog.see_page'), page_tab.facebook_url, target: '_blank', class: 'edit-tab-button' + - else + = button_to_function 'add', t('fb_app_plugin.views.myprofile.catalogs.new'), "$(this).toggle(); $('#page-tab-new h3, #add_tab').toggle(400)" + + = render 'fb_app_plugin_page_tab/configure_form', page_tab: page_tab, signed_request: nil diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_button.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_button.html.slim new file mode 100644 index 0000000..9103ed8 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_button.html.slim @@ -0,0 +1,7 @@ +#manage-fb-store-ctrl + - if @page_tab.owner_profile + - if logged_in? and (user.is_admin? environment or user.is_admin? @page_tab.owner_profile) + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_myprofile, profile: @page_tab.owner_profile.identifier}, + target: '_parent' + - elsif (@data[:page][:admin] rescue false) + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_page_tab, action: :admin, signed_request: @signed_requests, page_id: @page_ids}, onclick: "noosfero.modal.url(this.href); return false;" diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_form.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_form.html.slim new file mode 100644 index 0000000..3f3ffa7 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_form.html.slim @@ -0,0 +1,37 @@ +- form_id = if page_tab.id then "edit_tab_#{page_tab.id}" else "add_tab" end + +- unless page_tab.id + h3= t'fb_app_plugin.views.myprofile.catalogs.new' + += form_for page_tab, as: :page_tab, url: {controller: :fb_app_plugin_page_tab, action: :admin}, + html: {id: form_id, class: "edit-page-tab", onsubmit: "return fb_app.page_tab.config.save($(this))"} do |f| + + = hidden_field_tag :signed_request, signed_request + = hidden_field_tag :page_id, page_tab.page_id + = f.hidden_field :profile_id, value: profile.id + = f.hidden_field :page_id + + = f.label :title, t("fb_app_plugin.views.myprofile.catalogs.catalog_title_label") + = f.text_field :title, class: 'form-control' + + = f.label :subtitle, t("fb_app_plugin.views.myprofile.catalogs.catalog_subtitle_label") + = f.text_area :subtitle, class: 'form-control mceEditor', id: "page-tab-subtitle-#{page_tab.id}" + + = f.label :config_type, t("fb_app_plugin.views.myprofile.catalogs.catalog_type_chooser_label") + = f.select :config_type, + page_tab.types.map{ |type| [t("fb_app_plugin.models.page_tab.types.#{if profile.enterprise? and type == :profile then :other_profile else type end}"), type] }, + {}, onchange: 'fb_app.page_tab.config.change_type($(this))', class: 'form-control' + + - page_tab.types.each do |type| + div class="config-type config-type-#{type}" + = render "fb_app_plugin_page_tab/configure_#{type}", f: f, page_tab: page_tab + + - if page_tab.new_record? + = submit_button :add, t('fb_app_plugin.views.page_tab.add'), onclick: 'return fb_app.page_tab.config.add($(this.form))', class: 'fb-app-submit-page-tab-options' + - else + = submit_button :save, t('fb_app_plugin.views.page_tab.save'), class: 'fb-app-submit-page-tab-options' + +javascript: + $('document').ready(function() { + fb_app.page_tab.config.init(); + }); diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_own_profile.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_own_profile.html.slim new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_own_profile.html.slim @@ -0,0 +1 @@ + diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profile.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profile.html.slim new file mode 100644 index 0000000..3ddab43 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profile.html.slim @@ -0,0 +1,27 @@ += f.label t("fb_app_plugin.views.myprofile.catalogs.profile_chooser_label") += f.text_field :profile_ids, "data-limit" => "1", "data-placeholder" => t('fb_app_plugin.views.page_tab.profile.placeholder'), onchange: 'fb_app.page_tab.config.profile.onchange($(this))' + +javascript: + $(document).ready(function() { + var selector = '#page-tab-#{page_tab.id || 'new'} .config-type-profile #page_tab_profile_ids' + + fb_app.page_tab.config.profile.onchange($(selector)) + + open_graph.autocomplete.init( + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json}, + selector, + #{[page_tab.profile].compact.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json}, + {tokenfield: {limit: 1}} + ) + + if (#{page_tab.profile.present?.to_json}) + $(selector+'-tokenfield').hide() + + $(selector) + .on('tokenfield:createdtoken', function (e) { + $(selector+'-tokenfield').hide(); + }) + .on('tokenfield:removedtoken', function (e) { + $(selector+'-tokenfield').show(); + }) + }) diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profiles.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profiles.html.slim new file mode 100644 index 0000000..8b2f1ac --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profiles.html.slim @@ -0,0 +1,11 @@ += f.label t("fb_app_plugin.views.myprofile.catalogs.profiles_chooser_label") += f.text_field :profile_ids, placeholder: t('fb_app_plugin.views.page_tab.profiles.placeholder') + +javascript: + $(document).ready(function() { + open_graph.autocomplete.init( + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json}, + '#page-tab-#{page_tab.id || 'new'} .config-type-profiles #page_tab_profile_ids', + #{page_tab.profiles.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json} + ) + }) diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_query.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_query.html.slim new file mode 100644 index 0000000..df0cafb --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_configure_query.html.slim @@ -0,0 +1,4 @@ += f.label t("fb_app_plugin.views.myprofile.catalogs.query_label") +p= t'fb_app_plugin.views.myprofile.catalogs.query_help' += f.text_field :query, placeholder: t('fb_app_plugin.views.page_tab.query.placeholder'), class: 'form-control' + diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim new file mode 100644 index 0000000..96a7b84 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim @@ -0,0 +1,5 @@ +#page-tab-footer + #page-tab-footer1.col-lg-6.col-md-6.col-sm-6.text-left + = t'fb_app_plugin.views.page_tab.footer1' + #page-tab-footer2.col-lg-6.col-md-6.col-sm-6.text-right + = t'fb_app_plugin.views.page_tab.footer2' diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim new file mode 100644 index 0000000..ec0939f --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim @@ -0,0 +1,6 @@ +- callback = '' unless defined? callback + += render 'fb_app_plugin/load', callback: callback + +javascript: + fb_app.page_tab.init(); diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim new file mode 100644 index 0000000..0b2eb9a --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim @@ -0,0 +1,4 @@ +h1= @page_tab.title + +- if @page_tab.subtitle.present? + #page-tab-subtitle= @page_tab.subtitle diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim new file mode 100644 index 0000000..b570a6e --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim @@ -0,0 +1,6 @@ +#fb-app-page-tab-admin + = render 'admin_intro' rescue nil + + = render 'config', page_tab: @page_tab, page_id: @page_id, signed_request: @signed_request + = render file: 'shared/tiny_mce', locals: {mode: 'simple'} + diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb b/plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb new file mode 100644 index 0000000..8b55e89 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb @@ -0,0 +1,2 @@ +fb_app.page_tab.config.close(<%= @page_id.to_json %>); + diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/catalog.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/catalog.html.slim new file mode 100644 index 0000000..3b27e11 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/catalog.html.slim @@ -0,0 +1,21 @@ +#fb-app-catalog-wrapper class=('fb-app-standalone' if @signed_request.blank?) + + = render 'load' + = render 'title_and_subtitle' + + #product-catalog + #product-catalog-actions + - if @page_tab.config_type == :profile + - if profile and user.present? and (user.is_admin?(environment) or user.is_admin?(profile)) + .product-catalog-ctrl + = button :add, _('Add product or service'), controller: :manage_products, action: :new, profile: profile.identifier + = render 'configure_button' + = content_for :product_page do + = render 'catalog/results' + = render 'catalog/search' + = render 'catalog/javascripts', external: false + + = render 'footer' + + javascript: + catalog.base_url_path = #{url_for(controller: :fb_app_plugin_page_tab, action: :index, page_id: params[:page_id], signed_request: params[:signed_request]).to_json} + '&' diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim new file mode 100644 index 0000000..757b073 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim @@ -0,0 +1,3 @@ +javascript: + noosfero.modal.url(#{url_for(controller: :fb_app_plugin, action: :admin, page_id: @page_ids).to_json}) + diff --git a/plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim b/plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim new file mode 100644 index 0000000..41e4178 --- /dev/null +++ b/plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim @@ -0,0 +1,11 @@ += render 'title_and_subtitle' + += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: '' + += render file: "#{Rails.root}/app/views/manage_products/show.html.erb" + += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: '' + += render 'footer' + += render 'load' -- libgit2 0.21.2