From c2cf650fc6c05bd8722e849eccb3e2bc075c2701 Mon Sep 17 00:00:00 2001 From: Devin Zhang Date: Tue, 30 Oct 2012 19:54:29 +0800 Subject: [PATCH 001/181] first commit --- .gitignore | 27 + .rspec | 1 + Gemfile | 70 + Gemfile.lock | 249 +++ License.txt | 15 + README | 4 + Rakefile | 7 + app/assets/images/admin/ads.png | Bin 0 -> 1319 bytes app/assets/images/admin/bg.png | Bin 0 -> 12875 bytes app/assets/images/admin/cloud.png | Bin 0 -> 1792 bytes app/assets/images/admin/dashboard.png | Bin 0 -> 1693 bytes app/assets/images/admin/member.png | Bin 0 -> 3146 bytes app/assets/images/admin/nodes.png | Bin 0 -> 1374 bytes app/assets/images/admin/pages.png | Bin 0 -> 1280 bytes app/assets/images/admin/palette.png | Bin 0 -> 2094 bytes app/assets/images/admin/reward_history.png | Bin 0 -> 1890 bytes app/assets/images/admin/settings.png | Bin 0 -> 1951 bytes app/assets/images/admin/topics.png | Bin 0 -> 1315 bytes app/assets/images/admin/users.png | Bin 0 -> 1444 bytes app/assets/images/bg.png | Bin 0 -> 11296 bytes app/assets/images/bg_blended.png | Bin 0 -> 214 bytes app/assets/images/bg_top_light.png | Bin 0 -> 3568 bytes app/assets/images/dot_orange.png | Bin 0 -> 218 bytes app/assets/images/ghost.png | Bin 0 -> 787 bytes app/assets/images/heart-gray.png | Bin 0 -> 1109 bytes app/assets/images/heart.png | Bin 0 -> 1170 bytes app/assets/images/mobile/bg_section.png | Bin 0 -> 430 bytes app/assets/images/mobile/eject.png | Bin 0 -> 6574 bytes app/assets/images/mobile/eject48.png | Bin 0 -> 7490 bytes app/assets/images/mobile/gear.png | Bin 0 -> 7099 bytes app/assets/images/mobile/gear48.png | Bin 0 -> 9627 bytes app/assets/images/qbar.png | Bin 0 -> 2295 bytes app/assets/images/rails.png | Bin 0 -> 6646 bytes app/assets/images/reply_button.png | Bin 0 -> 1461 bytes app/assets/images/rss.png | Bin 0 -> 747 bytes app/assets/images/shadow.png | Bin 0 -> 1009 bytes app/assets/images/star.png | Bin 0 -> 1563 bytes app/assets/javascripts/application.js | 21 + app/assets/javascripts/rabel.js.coffee | 90 + .../stylesheets/admin/i_base.css.scss.erb | 43 + app/assets/stylesheets/application.css | 8 + app/assets/stylesheets/desktop.css.scss.erb | 998 +++++++++ app/assets/stylesheets/i_mobile.css.erb | 501 +++++ app/assets/stylesheets/rabel.css.scss.erb | 223 ++ app/assets/stylesheets/shared.css.scss | 215 ++ .../admin/advertisements_controller.rb | 51 + app/controllers/admin/base_controller.rb | 26 + .../admin/cloud_files_controller.rb | 34 + app/controllers/admin/nodes_controller.rb | 74 + .../admin/notifications_controller.rb | 8 + app/controllers/admin/pages_controller.rb | 59 + app/controllers/admin/planes_controller.rb | 71 + app/controllers/admin/rewards_controller.rb | 34 + .../admin/site_settings_controller.rb | 21 + app/controllers/admin/topics_controller.rb | 16 + app/controllers/admin/users_controller.rb | 88 + .../admin/welcome_admin_controller.rb | 7 + app/controllers/application_controller.rb | 127 ++ app/controllers/bookmarks_controller.rb | 24 + app/controllers/comments_controller.rb | 47 + app/controllers/nodes_controller.rb | 19 + app/controllers/notifications_controller.rb | 29 + app/controllers/pages_controller.rb | 10 + app/controllers/registrations_controller.rb | 21 + app/controllers/sessions_controller.rb | 14 + app/controllers/topics_controller.rb | 170 ++ app/controllers/users_controller.rb | 117 ++ app/controllers/welcome_controller.rb | 34 + app/helpers/admin/base_helper.rb | 51 + app/helpers/application_helper.rb | 212 ++ app/mailers/.gitkeep | 0 app/models/.gitkeep | 0 app/models/ability.rb | 42 + app/models/account.rb | 20 + app/models/advertisement.rb | 34 + app/models/bookmark.rb | 7 + app/models/cloud_file.rb | 14 + app/models/comment.rb | 77 + app/models/following.rb | 9 + app/models/node.rb | 24 + app/models/notifiable.rb | 16 + app/models/notification.rb | 44 + app/models/page.rb | 14 + app/models/plane.rb | 13 + app/models/reward.rb | 56 + app/models/settings.rb | 13 + app/models/siteconf.rb | 57 + app/models/sortable.rb | 23 + app/models/topic.rb | 108 + app/models/user.rb | 190 ++ app/uploaders/ad_banner_uploader.rb | 53 + app/uploaders/avatar_uploader.rb | 58 + app/uploaders/cloud_file_uploader.rb | 46 + app/uploaders/picture_extension_white_list.rb | 5 + app/uploaders/uploader_helper.rb | 19 + .../advertisements/_advertisement.html.haml | 22 + .../admin/advertisements/_form.html.haml | 58 + app/views/admin/advertisements/edit.html.haml | 5 + .../admin/advertisements/index.html.haml | 13 + app/views/admin/advertisements/new.html.haml | 5 + .../admin/cloud_files/_cloud_file.html.haml | 9 + app/views/admin/cloud_files/_form.html.haml | 16 + app/views/admin/cloud_files/index.html.haml | 18 + app/views/admin/cloud_files/new.html.haml | 5 + app/views/admin/nodes/_form.html.haml | 49 + app/views/admin/nodes/_move_form.js.haml | 6 + app/views/admin/nodes/_node.html.haml | 8 + app/views/admin/nodes/create.js.haml | 3 + app/views/admin/nodes/destroy.js.haml | 4 + app/views/admin/nodes/move.js.haml | 1 + app/views/admin/nodes/move_to.js.haml | 1 + app/views/admin/nodes/show_form.js.haml | 2 + app/views/admin/nodes/update.js.haml | 6 + app/views/admin/pages/_form.html.haml | 46 + app/views/admin/pages/_page.html.haml | 11 + app/views/admin/pages/action.html.haml | 5 + app/views/admin/pages/index.html.haml | 27 + app/views/admin/planes/_form.html.haml | 13 + app/views/admin/planes/_plane.html.haml | 11 + app/views/admin/planes/_sort_plane.html.haml | 1 + app/views/admin/planes/_sort_planes.html.haml | 4 + app/views/admin/planes/create.js.haml | 2 + app/views/admin/planes/destroy.js.haml | 1 + app/views/admin/planes/index.html.haml | 14 + app/views/admin/planes/show_form.js.haml | 1 + app/views/admin/planes/sort.js.haml | 2 + app/views/admin/rewards/_form.html.haml | 26 + app/views/admin/rewards/_reward.html.haml | 16 + app/views/admin/rewards/create.js.haml | 8 + app/views/admin/rewards/index.html.haml | 16 + app/views/admin/rewards/new.js.haml | 1 + .../admin/site_settings/appearance.html.haml | 67 + app/views/admin/site_settings/show.html.haml | 201 ++ app/views/admin/topics/_table_view.html.haml | 32 + app/views/admin/topics/index.html.haml | 8 + app/views/admin/users/_user.html.haml | 37 + app/views/admin/users/edit.html.haml | 38 + app/views/admin/users/index.html.haml | 23 + app/views/admin/users/toggle_admin.js.haml | 2 + app/views/admin/users/toggle_blocked.js.haml | 2 + app/views/admin/welcome_admin/index.html.haml | 55 + app/views/comments/_comment.html.haml | 31 + app/views/comments/_comment.mobile.haml | 19 + app/views/comments/_form.js.haml | 5 + app/views/comments/destroy.js.haml | 2 + app/views/comments/edit.js.haml | 2 + app/views/comments/update.js.haml | 2 + app/views/devise/confirmations/new.html.erb | 12 + .../mailer/confirmation_instructions.html.erb | 5 + .../reset_password_instructions.html.haml | 7 + .../mailer/unlock_instructions.html.erb | 7 + app/views/devise/passwords/edit.html.haml | 24 + app/views/devise/passwords/new.html.haml | 27 + .../devise/registrations/_form.html.haml | 33 + app/views/devise/registrations/edit.html.erb | 25 + app/views/devise/registrations/new.html.haml | 5 + .../devise/registrations/new.mobile.haml | 3 + app/views/devise/shared/_links.erb | 25 + app/views/devise/unlocks/new.html.erb | 12 + app/views/kaminari/_first_page.html.haml | 9 + app/views/kaminari/_first_page.mobile.haml | 9 + app/views/kaminari/_gap.html.haml | 8 + app/views/kaminari/_gap.mobile.haml | 8 + app/views/kaminari/_last_page.html.haml | 9 + app/views/kaminari/_last_page.mobile.haml | 9 + app/views/kaminari/_next_page.html.haml | 9 + app/views/kaminari/_next_page.mobile.haml | 9 + app/views/kaminari/_page.html.haml | 10 + app/views/kaminari/_page.mobile.haml | 10 + app/views/kaminari/_paginator.html.haml | 18 + app/views/kaminari/_paginator.mobile.haml | 18 + app/views/kaminari/_prev_page.html.haml | 9 + app/views/kaminari/_prev_page.mobile.haml | 9 + app/views/layouts/admin.html.haml | 28 + app/views/layouts/application.html.haml | 27 + app/views/layouts/application.mobile.haml | 38 + app/views/nodes/_bookmark_node.html.haml | 6 + app/views/nodes/_bookmark_node.mobile.haml | 1 + app/views/nodes/_custom_fields.html.haml | 13 + app/views/nodes/_item_node.html.haml | 1 + app/views/nodes/_node.html.haml | 1 + app/views/nodes/_node.mobile.haml | 2 + app/views/nodes/_paginator.html.haml | 18 + app/views/nodes/show.html.haml | 44 + app/views/nodes/show.mobile.haml | 16 + .../notifications/_notification.mobile.haml | 5 + app/views/notifications/index.html.haml | 6 + app/views/notifications/index.mobile.haml | 1 + app/views/pages/_nav.html.haml | 6 + app/views/pages/_nav_ruler.html.haml | 1 + app/views/pages/show.html.haml | 15 + app/views/pages/show.mobile.haml | 4 + app/views/planes/_plane.html.haml | 7 + app/views/planes/_plane.mobile.haml | 7 + app/views/sessions/new.html.haml | 28 + app/views/sessions/new.mobile.haml | 24 + app/views/shared/_ad.html.haml | 15 + app/views/shared/_box_tip.html.haml | 3 + app/views/shared/_community_stats.html.haml | 21 + app/views/shared/_content.html.haml | 7 + app/views/shared/_footer.html.haml | 21 + app/views/shared/_google_analytics.html.haml | 12 + app/views/shared/_head.html.haml | 9 + app/views/shared/_meta.html.haml | 8 + app/views/shared/_my_fav.html.haml | 11 + app/views/shared/_notification.html.haml | 24 + app/views/shared/_preview_widget.html.haml | 3 + app/views/shared/_rss.html.haml | 2 + app/views/shared/_sidebar_box.html.haml | 42 + app/views/shared/_top.html.haml | 21 + app/views/topics/_back_to_node.html.haml | 4 + app/views/topics/_complex_topic.html.haml | 28 + app/views/topics/_form.html.haml | 17 + app/views/topics/_move_form.js.haml | 6 + app/views/topics/_profile_topic.mobile.haml | 11 + app/views/topics/_simple_topic.html.haml | 14 + app/views/topics/_table_view.html.haml | 27 + app/views/topics/_title_form.html.haml | 7 + app/views/topics/_topic.html.haml | 5 + app/views/topics/_topic.mobile.haml | 24 + app/views/topics/edit.html.haml | 32 + app/views/topics/edit_title.js.haml | 1 + app/views/topics/index.atom.builder | 16 + app/views/topics/index.html.haml | 5 + app/views/topics/move.js.haml | 1 + app/views/topics/new.html.haml | 6 + app/views/topics/new.mobile.haml | 14 + app/views/topics/show.html.haml | 56 + app/views/topics/show.mobile.haml | 52 + .../topics/show/_bookmark_button.html.haml | 8 + .../topics/show/_bookmarked_users.html.haml | 9 + app/views/topics/show/_comment_form.html.haml | 15 + app/views/topics/show/_comments.html.haml | 16 + app/views/topics/show/_manage.html.haml | 19 + app/views/topics/update_title.js.haml | 2 + .../users/_account_detail_form.html.haml | 22 + app/views/users/_account_form.html.haml | 16 + app/views/users/_followed_ruler.html.haml | 3 + app/views/users/_followed_user.html.haml | 13 + app/views/users/_password_form.html.haml | 20 + app/views/users/_user.mobile.haml | 9 + app/views/users/edit.html.haml | 42 + app/views/users/edit.mobile.haml | 8 + app/views/users/my_following.html.haml | 16 + app/views/users/my_following.mobile.haml | 3 + app/views/users/my_topics.html.haml | 4 + app/views/users/my_topics.mobile.haml | 1 + app/views/users/show.html.haml | 112 + app/views/users/show.mobile.haml | 35 + app/views/users/topics.html.haml | 9 + app/views/welcome/_home_planes.html.haml | 6 + app/views/welcome/_home_topics.html.haml | 23 + app/views/welcome/_rightbar_plane.html.haml | 4 + app/views/welcome/_rightbar_planes.html.haml | 6 + app/views/welcome/exception.html.haml | 15 + app/views/welcome/exception.mobile.haml | 7 + app/views/welcome/goodbye.html.haml | 10 + app/views/welcome/goodbye.mobile.haml | 4 + app/views/welcome/index.html.haml | 19 + app/views/welcome/index.mobile.haml | 12 + config.ru | 4 + config/application.rb | 64 + config/boot.rb | 6 + config/cucumber.yml | 8 + config/database.yml.mysql | 33 + config/database.yml.pg | 47 + config/deploy/example.conf | 49 + config/environment.rb | 5 + config/environments/development.rb | 39 + config/environments/production.rb | 61 + config/environments/test.rb | 43 + config/initializers/backtrace_silencers.rb | 7 + config/initializers/carrierwave.rb | 1 + config/initializers/devise.rb | 207 ++ config/initializers/form_error.rb | 11 + config/initializers/inflections.rb | 10 + config/initializers/kaminari.rb | 9 + config/initializers/markdown.rb | 145 ++ config/initializers/mime_types.rb | 5 + config/initializers/monkey_patch.rb | 32 + config/initializers/rabel.rb | 36 + config/initializers/secret_token.rb | 7 + config/initializers/session_store.rb | 8 + config/initializers/version.rb | 14 + config/initializers/wrap_parameters.rb | 14 + config/locales/devise.en.yml | 59 + config/locales/devise.zh.yml | 62 + config/locales/en.yml | 5 + config/locales/zh.yml | 241 +++ config/routes.rb | 133 ++ config/settings.yml.example | 16 + .../20111207114903_devise_create_users.rb | 54 + .../20111207115533_add_nickname_to_users.rb | 6 + db/migrate/20111208133931_create_accounts.rb | 14 + .../20111208134052_add_index_to_accounts.rb | 5 + db/migrate/20111209053551_create_nodes.rb | 12 + .../20111209053640_add_index_to_nodes.rb | 5 + db/migrate/20111209054915_create_topics.rb | 13 + .../20111209054959_add_index_to_topics.rb | 6 + db/migrate/20111209060937_create_comments.rb | 12 + .../20111209061125_add_index_to_comments.rb | 6 + db/migrate/20111209080245_create_planes.rb | 9 + .../20111209080432_add_plane_id_to_nodes.rb | 6 + db/migrate/20111217060752_create_bookmarks.rb | 11 + .../20111217061017_add_index_to_bookmarks.rb | 8 + .../20111220135343_create_followings.rb | 10 + .../20111220135535_add_index_to_followings.rb | 7 + .../20111224055101_add_avatar_to_users.rb | 5 + db/migrate/20111224105955_create_pages.rb | 11 + .../20111224111225_add_index_to_pages.rb | 5 + .../20111224111624_add_admin_to_users.rb | 6 + .../20111230121459_create_notifications.rb | 15 + ...111230121950_add_index_to_notifications.rb | 8 + ...0165404_add_more_index_to_notifications.rb | 7 + .../20120104115917_add_published_to_pages.rb | 6 + .../20120106055211_add_role_to_users.rb | 7 + .../20120107134203_create_advertisements.rb | 15 + ...20107134636_add_index_to_advertisements.rb | 8 + .../20120205011606_add_position_to_pages.rb | 6 + .../20120206084156_add_position_to_nodes.rb | 6 + .../20120217131718_add_blocked_to_users.rb | 5 + db/migrate/20120304140424_create_settings.rb | 17 + ...16074203_add_updated_at_index_to_topics.rb | 5 + ...0120316134205_add_involved_at_to_topics.rb | 16 + ...142416_add_created_at_index_to_comments.rb | 5 + ...070333_add_updated_at_index_to_comments.rb | 5 + ...04100838_add_updated_at_index_to_planes.rb | 5 + ...404101032_add_updated_at_index_to_nodes.rb | 5 + ...0405045224_add_comments_count_to_topics.rb | 5 + ...0405064209_set_comments_count_on_topics.rb | 13 + ...0120405071656_add_topics_count_to_nodes.rb | 5 + ...0120405072538_set_topics_count_on_nodes.rb | 13 + .../20120427122531_add_position_to_planes.rb | 5 + .../20120428141950_create_cloud_files.rb | 12 + ...3738_add_created_at_index_to_followings.rb | 5 + ...23133345_add_posting_device_to_comments.rb | 5 + ...624064847_add_comments_closed_to_topics.rb | 5 + .../20120624124831_add_quiet_to_nodes.rb | 6 + .../20120625011328_add_sticky_to_topics.rb | 6 + .../20120626085113_add_reward_to_users.rb | 5 + db/migrate/20120627121746_create_rewards.rb | 14 + .../20120627194105_add_custom_css_to_nodes.rb | 5 + ...722021548_add_last_replied_by_to_topics.rb | 12 + ...120722031529_add_weibo_link_to_accounts.rb | 14 + ...727092241_add_last_replied_at_to_topics.rb | 11 + db/schema.rb | 218 ++ db/seeds.rb | 47 + deploy/auto_start_thin_centos.sh | 5 + deploy/auto_start_thin_ubuntu.sh | 4 + deploy/create_thin_config.sh | 9 + deploy/install_bundle.sh | 1 + deploy/migrate_database.sh | 1 + deploy/precompile_assets.sh | 1 + deploy/setup_database_once.sh | 1 + deploy/thin_auto_start_ubuntu.sh | 4 + deploy/ubuntu_12.04_install.sh | 43 + deploy/upgrade_rabel.sh | 59 + doc/README_FOR_APP | 2 + features/admin/ads.feature | 33 + features/admin/base.feature | 16 + features/admin/nodes.feature | 33 + features/admin/pages.feature | 55 + features/admin/planes.feature | 44 + features/admin/topics.feature | 12 + features/admin/users.feature | 23 + features/bookmarks.feature | 23 + features/comments.feature | 31 + features/homepage.feature | 43 + features/mobile/base.feature | 124 ++ features/mobile/bookmark.feature | 30 + features/mobile/follow.feature | 26 + features/mobile/notification.feature | 13 + features/mobile/topic.feature | 10 + features/nodes.feature | 54 + features/pages.feature | 20 + features/passwords.feature | 9 + features/session.feature | 17 + features/step_definitions/ad.rb | 6 + features/step_definitions/base.rb | 267 +++ features/step_definitions/bookmark.rb | 35 + features/step_definitions/comments.rb | 18 + features/step_definitions/mobile.rb | 30 + features/step_definitions/node.rb | 55 + features/step_definitions/page.rb | 37 + features/step_definitions/password.rb | 7 + features/step_definitions/plane.rb | 23 + features/step_definitions/session.rb | 23 + features/step_definitions/topic.rb | 106 + features/step_definitions/user.rb | 63 + features/support/env.rb | 75 + features/support/named_routes_fix.rb | 23 + features/topics.feature | 162 ++ features/users.feature | 87 + lib/active_support/cache/null_store.rb | 30 + lib/assets/.gitkeep | 0 lib/assets/images/icon/location.png | Bin 0 -> 632 bytes lib/assets/images/icon/mobileme.png | Bin 0 -> 565 bytes lib/assets/images/icon/sina_weibo.png | Bin 0 -> 3412 bytes lib/assets/images/icon/twitter.png | Bin 0 -> 568 bytes lib/assets/images/icon/tx_weibo.png | Bin 0 -> 42878 bytes lib/rabel/active_cache.rb | 121 ++ lib/rabel/base.rb | 66 + lib/rabel/captcha.rb | 34 + lib/rabel/link_email_parser.rb | 66 + lib/rabel/model.rb | 8 + lib/tasks/.gitkeep | 0 lib/tasks/cucumber.rake | 65 + log/.gitkeep | 0 public/404.html | 26 + public/422.html | 26 + public/500.html | 26 + public/avatar/default.png | Bin 0 -> 5891 bytes public/avatar/medium_default.png | Bin 0 -> 2800 bytes public/avatar/mini_default.png | Bin 0 -> 1073 bytes public/banner/default.gif | Bin 0 -> 414 bytes public/favicon.ico | Bin 0 -> 1150 bytes public/robots.txt | 5 + script/cucumber | 10 + script/rails | 6 + .../admin/advertisements_controller_spec.rb | 40 + .../admin/cloud_files_controller_spec.rb | 5 + .../admin/nodes_controller_spec.rb | 71 + .../admin/notifications_controller_spec.rb | 5 + .../admin/pages_controller_spec.rb | 67 + .../admin/planes_controller_spec.rb | 67 + .../admin/rewards_controller_spec.rb | 5 + .../admin/site_settings_controller_spec.rb | 16 + .../admin/topics_controller_spec.rb | 15 + .../admin/users_controller_spec.rb | 40 + .../admin/welcome_admin_controller_spec.rb | 39 + spec/controllers/bookmarks_controller_spec.rb | 70 + spec/controllers/comments_controller_spec.rb | 74 + spec/controllers/nodes_controller_spec.rb | 25 + .../notifications_controller_spec.rb | 22 + spec/controllers/pages_controller_spec.rb | 29 + .../registrations_controller_spec.rb | 5 + spec/controllers/sessions_controller_spec.rb | 11 + spec/controllers/topics_controller_spec.rb | 232 +++ spec/controllers/users_controller_spec.rb | 137 ++ spec/controllers/welcome_controller_spec.rb | 26 + spec/helpers/application_helper_spec.rb | 59 + spec/models/account_spec.rb | 7 + spec/models/advertisement_spec.rb | 13 + spec/models/bookmark_spec.rb | 12 + spec/models/cloud_file_spec.rb | 9 + spec/models/comment_spec.rb | 33 + spec/models/following_spec.rb | 6 + spec/models/node_spec.rb | 22 + spec/models/notification_spec.rb | 13 + spec/models/page_spec.rb | 12 + spec/models/plane_spec.rb | 9 + spec/models/reward_spec.rb | 4 + spec/models/topic_spec.rb | 31 + spec/models/user_spec.rb | 67 + spec/requests/users_spec.rb | 26 + spec/spec_helper.rb | 52 + spec/support/controller_macros.rb | 38 + spec/support/controller_matchers.rb | 26 + test/factories/accounts.rb | 12 + test/factories/advertisements.rb | 12 + test/factories/bookmarks.rb | 8 + test/factories/comments.rb | 9 + test/factories/nodes.rb | 12 + test/factories/notifications.rb | 11 + test/factories/pages.rb | 10 + test/factories/planes.rb | 7 + test/factories/topics.rb | 20 + test/factories/users.rb | 19 + test/support/ads/linode.png | Bin 0 -> 5046 bytes themes/images/ruby_china/bg.png | Bin 0 -> 16947 bytes themes/stylesheets/ruby_china/i_theme.css.erb | 31 + .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 178 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 120 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 111 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 110 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 101 bytes .../assets/images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes .../assets/images/ui-icons_454545_256x240.png | Bin 0 -> 4369 bytes .../assets/javascripts/jquery_effects_core.js | 763 +++++++ .../javascripts/jquery_effects_highlight.js | 50 + vendor/assets/javascripts/jquery_elastic.js | 164 ++ vendor/assets/javascripts/jquery_hotkeys.js | 102 + .../javascripts/jquery_smooth_scroll.js | 192 ++ vendor/assets/javascripts/jquery_ui_core.js | 314 +++ .../javascripts/jquery_ui_datepicker.js | 1823 +++++++++++++++++ .../javascripts/jquery_ui_datepicker_cn.js | 23 + vendor/assets/javascripts/jquery_ui_mouse.js | 162 ++ .../assets/javascripts/jquery_ui_sortable.js | 1076 ++++++++++ vendor/assets/javascripts/jquery_ui_widget.js | 272 +++ vendor/assets/stylesheets/.gitkeep | 0 vendor/assets/stylesheets/jquery_ui.css.erb | 357 ++++ vendor/plugins/.gitkeep | 0 493 files changed, 18661 insertions(+) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 License.txt create mode 100644 README create mode 100644 Rakefile create mode 100644 app/assets/images/admin/ads.png create mode 100644 app/assets/images/admin/bg.png create mode 100644 app/assets/images/admin/cloud.png create mode 100644 app/assets/images/admin/dashboard.png create mode 100644 app/assets/images/admin/member.png create mode 100644 app/assets/images/admin/nodes.png create mode 100644 app/assets/images/admin/pages.png create mode 100644 app/assets/images/admin/palette.png create mode 100644 app/assets/images/admin/reward_history.png create mode 100644 app/assets/images/admin/settings.png create mode 100644 app/assets/images/admin/topics.png create mode 100644 app/assets/images/admin/users.png create mode 100644 app/assets/images/bg.png create mode 100644 app/assets/images/bg_blended.png create mode 100644 app/assets/images/bg_top_light.png create mode 100644 app/assets/images/dot_orange.png create mode 100644 app/assets/images/ghost.png create mode 100644 app/assets/images/heart-gray.png create mode 100644 app/assets/images/heart.png create mode 100644 app/assets/images/mobile/bg_section.png create mode 100644 app/assets/images/mobile/eject.png create mode 100644 app/assets/images/mobile/eject48.png create mode 100644 app/assets/images/mobile/gear.png create mode 100644 app/assets/images/mobile/gear48.png create mode 100644 app/assets/images/qbar.png create mode 100644 app/assets/images/rails.png create mode 100644 app/assets/images/reply_button.png create mode 100644 app/assets/images/rss.png create mode 100644 app/assets/images/shadow.png create mode 100644 app/assets/images/star.png create mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/javascripts/rabel.js.coffee create mode 100644 app/assets/stylesheets/admin/i_base.css.scss.erb create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/assets/stylesheets/desktop.css.scss.erb create mode 100644 app/assets/stylesheets/i_mobile.css.erb create mode 100644 app/assets/stylesheets/rabel.css.scss.erb create mode 100644 app/assets/stylesheets/shared.css.scss create mode 100644 app/controllers/admin/advertisements_controller.rb create mode 100644 app/controllers/admin/base_controller.rb create mode 100644 app/controllers/admin/cloud_files_controller.rb create mode 100644 app/controllers/admin/nodes_controller.rb create mode 100644 app/controllers/admin/notifications_controller.rb create mode 100644 app/controllers/admin/pages_controller.rb create mode 100644 app/controllers/admin/planes_controller.rb create mode 100644 app/controllers/admin/rewards_controller.rb create mode 100644 app/controllers/admin/site_settings_controller.rb create mode 100644 app/controllers/admin/topics_controller.rb create mode 100644 app/controllers/admin/users_controller.rb create mode 100644 app/controllers/admin/welcome_admin_controller.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/bookmarks_controller.rb create mode 100644 app/controllers/comments_controller.rb create mode 100644 app/controllers/nodes_controller.rb create mode 100644 app/controllers/notifications_controller.rb create mode 100644 app/controllers/pages_controller.rb create mode 100644 app/controllers/registrations_controller.rb create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/controllers/topics_controller.rb create mode 100644 app/controllers/users_controller.rb create mode 100644 app/controllers/welcome_controller.rb create mode 100644 app/helpers/admin/base_helper.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/mailers/.gitkeep create mode 100644 app/models/.gitkeep create mode 100644 app/models/ability.rb create mode 100644 app/models/account.rb create mode 100644 app/models/advertisement.rb create mode 100644 app/models/bookmark.rb create mode 100644 app/models/cloud_file.rb create mode 100644 app/models/comment.rb create mode 100644 app/models/following.rb create mode 100644 app/models/node.rb create mode 100644 app/models/notifiable.rb create mode 100644 app/models/notification.rb create mode 100644 app/models/page.rb create mode 100644 app/models/plane.rb create mode 100644 app/models/reward.rb create mode 100644 app/models/settings.rb create mode 100644 app/models/siteconf.rb create mode 100644 app/models/sortable.rb create mode 100644 app/models/topic.rb create mode 100644 app/models/user.rb create mode 100644 app/uploaders/ad_banner_uploader.rb create mode 100644 app/uploaders/avatar_uploader.rb create mode 100644 app/uploaders/cloud_file_uploader.rb create mode 100644 app/uploaders/picture_extension_white_list.rb create mode 100644 app/uploaders/uploader_helper.rb create mode 100644 app/views/admin/advertisements/_advertisement.html.haml create mode 100644 app/views/admin/advertisements/_form.html.haml create mode 100644 app/views/admin/advertisements/edit.html.haml create mode 100644 app/views/admin/advertisements/index.html.haml create mode 100644 app/views/admin/advertisements/new.html.haml create mode 100644 app/views/admin/cloud_files/_cloud_file.html.haml create mode 100644 app/views/admin/cloud_files/_form.html.haml create mode 100644 app/views/admin/cloud_files/index.html.haml create mode 100644 app/views/admin/cloud_files/new.html.haml create mode 100644 app/views/admin/nodes/_form.html.haml create mode 100644 app/views/admin/nodes/_move_form.js.haml create mode 100644 app/views/admin/nodes/_node.html.haml create mode 100644 app/views/admin/nodes/create.js.haml create mode 100644 app/views/admin/nodes/destroy.js.haml create mode 100644 app/views/admin/nodes/move.js.haml create mode 100644 app/views/admin/nodes/move_to.js.haml create mode 100644 app/views/admin/nodes/show_form.js.haml create mode 100644 app/views/admin/nodes/update.js.haml create mode 100644 app/views/admin/pages/_form.html.haml create mode 100644 app/views/admin/pages/_page.html.haml create mode 100644 app/views/admin/pages/action.html.haml create mode 100644 app/views/admin/pages/index.html.haml create mode 100644 app/views/admin/planes/_form.html.haml create mode 100644 app/views/admin/planes/_plane.html.haml create mode 100644 app/views/admin/planes/_sort_plane.html.haml create mode 100644 app/views/admin/planes/_sort_planes.html.haml create mode 100644 app/views/admin/planes/create.js.haml create mode 100644 app/views/admin/planes/destroy.js.haml create mode 100644 app/views/admin/planes/index.html.haml create mode 100644 app/views/admin/planes/show_form.js.haml create mode 100644 app/views/admin/planes/sort.js.haml create mode 100644 app/views/admin/rewards/_form.html.haml create mode 100644 app/views/admin/rewards/_reward.html.haml create mode 100644 app/views/admin/rewards/create.js.haml create mode 100644 app/views/admin/rewards/index.html.haml create mode 100644 app/views/admin/rewards/new.js.haml create mode 100644 app/views/admin/site_settings/appearance.html.haml create mode 100644 app/views/admin/site_settings/show.html.haml create mode 100644 app/views/admin/topics/_table_view.html.haml create mode 100644 app/views/admin/topics/index.html.haml create mode 100644 app/views/admin/users/_user.html.haml create mode 100644 app/views/admin/users/edit.html.haml create mode 100644 app/views/admin/users/index.html.haml create mode 100644 app/views/admin/users/toggle_admin.js.haml create mode 100644 app/views/admin/users/toggle_blocked.js.haml create mode 100644 app/views/admin/welcome_admin/index.html.haml create mode 100644 app/views/comments/_comment.html.haml create mode 100644 app/views/comments/_comment.mobile.haml create mode 100644 app/views/comments/_form.js.haml create mode 100644 app/views/comments/destroy.js.haml create mode 100644 app/views/comments/edit.js.haml create mode 100644 app/views/comments/update.js.haml create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.haml create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.haml create mode 100644 app/views/devise/passwords/new.html.haml create mode 100644 app/views/devise/registrations/_form.html.haml create mode 100644 app/views/devise/registrations/edit.html.erb create mode 100644 app/views/devise/registrations/new.html.haml create mode 100644 app/views/devise/registrations/new.mobile.haml create mode 100644 app/views/devise/shared/_links.erb create mode 100644 app/views/devise/unlocks/new.html.erb create mode 100644 app/views/kaminari/_first_page.html.haml create mode 100644 app/views/kaminari/_first_page.mobile.haml create mode 100644 app/views/kaminari/_gap.html.haml create mode 100644 app/views/kaminari/_gap.mobile.haml create mode 100644 app/views/kaminari/_last_page.html.haml create mode 100644 app/views/kaminari/_last_page.mobile.haml create mode 100644 app/views/kaminari/_next_page.html.haml create mode 100644 app/views/kaminari/_next_page.mobile.haml create mode 100644 app/views/kaminari/_page.html.haml create mode 100644 app/views/kaminari/_page.mobile.haml create mode 100644 app/views/kaminari/_paginator.html.haml create mode 100644 app/views/kaminari/_paginator.mobile.haml create mode 100644 app/views/kaminari/_prev_page.html.haml create mode 100644 app/views/kaminari/_prev_page.mobile.haml create mode 100644 app/views/layouts/admin.html.haml create mode 100644 app/views/layouts/application.html.haml create mode 100644 app/views/layouts/application.mobile.haml create mode 100644 app/views/nodes/_bookmark_node.html.haml create mode 100644 app/views/nodes/_bookmark_node.mobile.haml create mode 100644 app/views/nodes/_custom_fields.html.haml create mode 100644 app/views/nodes/_item_node.html.haml create mode 100644 app/views/nodes/_node.html.haml create mode 100644 app/views/nodes/_node.mobile.haml create mode 100644 app/views/nodes/_paginator.html.haml create mode 100644 app/views/nodes/show.html.haml create mode 100644 app/views/nodes/show.mobile.haml create mode 100644 app/views/notifications/_notification.mobile.haml create mode 100644 app/views/notifications/index.html.haml create mode 100644 app/views/notifications/index.mobile.haml create mode 100644 app/views/pages/_nav.html.haml create mode 100644 app/views/pages/_nav_ruler.html.haml create mode 100644 app/views/pages/show.html.haml create mode 100644 app/views/pages/show.mobile.haml create mode 100644 app/views/planes/_plane.html.haml create mode 100644 app/views/planes/_plane.mobile.haml create mode 100644 app/views/sessions/new.html.haml create mode 100644 app/views/sessions/new.mobile.haml create mode 100644 app/views/shared/_ad.html.haml create mode 100644 app/views/shared/_box_tip.html.haml create mode 100644 app/views/shared/_community_stats.html.haml create mode 100644 app/views/shared/_content.html.haml create mode 100644 app/views/shared/_footer.html.haml create mode 100644 app/views/shared/_google_analytics.html.haml create mode 100644 app/views/shared/_head.html.haml create mode 100644 app/views/shared/_meta.html.haml create mode 100644 app/views/shared/_my_fav.html.haml create mode 100644 app/views/shared/_notification.html.haml create mode 100644 app/views/shared/_preview_widget.html.haml create mode 100644 app/views/shared/_rss.html.haml create mode 100644 app/views/shared/_sidebar_box.html.haml create mode 100644 app/views/shared/_top.html.haml create mode 100644 app/views/topics/_back_to_node.html.haml create mode 100644 app/views/topics/_complex_topic.html.haml create mode 100644 app/views/topics/_form.html.haml create mode 100644 app/views/topics/_move_form.js.haml create mode 100644 app/views/topics/_profile_topic.mobile.haml create mode 100644 app/views/topics/_simple_topic.html.haml create mode 100644 app/views/topics/_table_view.html.haml create mode 100644 app/views/topics/_title_form.html.haml create mode 100644 app/views/topics/_topic.html.haml create mode 100644 app/views/topics/_topic.mobile.haml create mode 100644 app/views/topics/edit.html.haml create mode 100644 app/views/topics/edit_title.js.haml create mode 100644 app/views/topics/index.atom.builder create mode 100644 app/views/topics/index.html.haml create mode 100644 app/views/topics/move.js.haml create mode 100644 app/views/topics/new.html.haml create mode 100644 app/views/topics/new.mobile.haml create mode 100644 app/views/topics/show.html.haml create mode 100644 app/views/topics/show.mobile.haml create mode 100644 app/views/topics/show/_bookmark_button.html.haml create mode 100644 app/views/topics/show/_bookmarked_users.html.haml create mode 100644 app/views/topics/show/_comment_form.html.haml create mode 100644 app/views/topics/show/_comments.html.haml create mode 100644 app/views/topics/show/_manage.html.haml create mode 100644 app/views/topics/update_title.js.haml create mode 100644 app/views/users/_account_detail_form.html.haml create mode 100644 app/views/users/_account_form.html.haml create mode 100644 app/views/users/_followed_ruler.html.haml create mode 100644 app/views/users/_followed_user.html.haml create mode 100644 app/views/users/_password_form.html.haml create mode 100644 app/views/users/_user.mobile.haml create mode 100644 app/views/users/edit.html.haml create mode 100644 app/views/users/edit.mobile.haml create mode 100644 app/views/users/my_following.html.haml create mode 100644 app/views/users/my_following.mobile.haml create mode 100644 app/views/users/my_topics.html.haml create mode 100644 app/views/users/my_topics.mobile.haml create mode 100644 app/views/users/show.html.haml create mode 100644 app/views/users/show.mobile.haml create mode 100644 app/views/users/topics.html.haml create mode 100644 app/views/welcome/_home_planes.html.haml create mode 100644 app/views/welcome/_home_topics.html.haml create mode 100644 app/views/welcome/_rightbar_plane.html.haml create mode 100644 app/views/welcome/_rightbar_planes.html.haml create mode 100644 app/views/welcome/exception.html.haml create mode 100644 app/views/welcome/exception.mobile.haml create mode 100644 app/views/welcome/goodbye.html.haml create mode 100644 app/views/welcome/goodbye.mobile.haml create mode 100644 app/views/welcome/index.html.haml create mode 100644 app/views/welcome/index.mobile.haml create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/cucumber.yml create mode 100644 config/database.yml.mysql create mode 100644 config/database.yml.pg create mode 100644 config/deploy/example.conf create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/carrierwave.rb create mode 100644 config/initializers/devise.rb create mode 100644 config/initializers/form_error.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/kaminari.rb create mode 100644 config/initializers/markdown.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/monkey_patch.rb create mode 100644 config/initializers/rabel.rb create mode 100644 config/initializers/secret_token.rb create mode 100644 config/initializers/session_store.rb create mode 100644 config/initializers/version.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/devise.en.yml create mode 100644 config/locales/devise.zh.yml create mode 100644 config/locales/en.yml create mode 100644 config/locales/zh.yml create mode 100644 config/routes.rb create mode 100644 config/settings.yml.example create mode 100644 db/migrate/20111207114903_devise_create_users.rb create mode 100644 db/migrate/20111207115533_add_nickname_to_users.rb create mode 100644 db/migrate/20111208133931_create_accounts.rb create mode 100644 db/migrate/20111208134052_add_index_to_accounts.rb create mode 100644 db/migrate/20111209053551_create_nodes.rb create mode 100644 db/migrate/20111209053640_add_index_to_nodes.rb create mode 100644 db/migrate/20111209054915_create_topics.rb create mode 100644 db/migrate/20111209054959_add_index_to_topics.rb create mode 100644 db/migrate/20111209060937_create_comments.rb create mode 100644 db/migrate/20111209061125_add_index_to_comments.rb create mode 100644 db/migrate/20111209080245_create_planes.rb create mode 100644 db/migrate/20111209080432_add_plane_id_to_nodes.rb create mode 100644 db/migrate/20111217060752_create_bookmarks.rb create mode 100644 db/migrate/20111217061017_add_index_to_bookmarks.rb create mode 100644 db/migrate/20111220135343_create_followings.rb create mode 100644 db/migrate/20111220135535_add_index_to_followings.rb create mode 100644 db/migrate/20111224055101_add_avatar_to_users.rb create mode 100644 db/migrate/20111224105955_create_pages.rb create mode 100644 db/migrate/20111224111225_add_index_to_pages.rb create mode 100644 db/migrate/20111224111624_add_admin_to_users.rb create mode 100644 db/migrate/20111230121459_create_notifications.rb create mode 100644 db/migrate/20111230121950_add_index_to_notifications.rb create mode 100644 db/migrate/20111230165404_add_more_index_to_notifications.rb create mode 100644 db/migrate/20120104115917_add_published_to_pages.rb create mode 100644 db/migrate/20120106055211_add_role_to_users.rb create mode 100644 db/migrate/20120107134203_create_advertisements.rb create mode 100644 db/migrate/20120107134636_add_index_to_advertisements.rb create mode 100644 db/migrate/20120205011606_add_position_to_pages.rb create mode 100644 db/migrate/20120206084156_add_position_to_nodes.rb create mode 100644 db/migrate/20120217131718_add_blocked_to_users.rb create mode 100644 db/migrate/20120304140424_create_settings.rb create mode 100644 db/migrate/20120316074203_add_updated_at_index_to_topics.rb create mode 100644 db/migrate/20120316134205_add_involved_at_to_topics.rb create mode 100644 db/migrate/20120331142416_add_created_at_index_to_comments.rb create mode 100644 db/migrate/20120404070333_add_updated_at_index_to_comments.rb create mode 100644 db/migrate/20120404100838_add_updated_at_index_to_planes.rb create mode 100644 db/migrate/20120404101032_add_updated_at_index_to_nodes.rb create mode 100644 db/migrate/20120405045224_add_comments_count_to_topics.rb create mode 100644 db/migrate/20120405064209_set_comments_count_on_topics.rb create mode 100644 db/migrate/20120405071656_add_topics_count_to_nodes.rb create mode 100644 db/migrate/20120405072538_set_topics_count_on_nodes.rb create mode 100644 db/migrate/20120427122531_add_position_to_planes.rb create mode 100644 db/migrate/20120428141950_create_cloud_files.rb create mode 100644 db/migrate/20120622053738_add_created_at_index_to_followings.rb create mode 100644 db/migrate/20120623133345_add_posting_device_to_comments.rb create mode 100644 db/migrate/20120624064847_add_comments_closed_to_topics.rb create mode 100644 db/migrate/20120624124831_add_quiet_to_nodes.rb create mode 100644 db/migrate/20120625011328_add_sticky_to_topics.rb create mode 100644 db/migrate/20120626085113_add_reward_to_users.rb create mode 100644 db/migrate/20120627121746_create_rewards.rb create mode 100644 db/migrate/20120627194105_add_custom_css_to_nodes.rb create mode 100644 db/migrate/20120722021548_add_last_replied_by_to_topics.rb create mode 100644 db/migrate/20120722031529_add_weibo_link_to_accounts.rb create mode 100644 db/migrate/20120727092241_add_last_replied_at_to_topics.rb create mode 100644 db/schema.rb create mode 100644 db/seeds.rb create mode 100755 deploy/auto_start_thin_centos.sh create mode 100755 deploy/auto_start_thin_ubuntu.sh create mode 100755 deploy/create_thin_config.sh create mode 100755 deploy/install_bundle.sh create mode 100755 deploy/migrate_database.sh create mode 100755 deploy/precompile_assets.sh create mode 100755 deploy/setup_database_once.sh create mode 100755 deploy/thin_auto_start_ubuntu.sh create mode 100755 deploy/ubuntu_12.04_install.sh create mode 100755 deploy/upgrade_rabel.sh create mode 100644 doc/README_FOR_APP create mode 100644 features/admin/ads.feature create mode 100644 features/admin/base.feature create mode 100644 features/admin/nodes.feature create mode 100644 features/admin/pages.feature create mode 100644 features/admin/planes.feature create mode 100644 features/admin/topics.feature create mode 100644 features/admin/users.feature create mode 100644 features/bookmarks.feature create mode 100644 features/comments.feature create mode 100644 features/homepage.feature create mode 100644 features/mobile/base.feature create mode 100644 features/mobile/bookmark.feature create mode 100644 features/mobile/follow.feature create mode 100644 features/mobile/notification.feature create mode 100644 features/mobile/topic.feature create mode 100644 features/nodes.feature create mode 100644 features/pages.feature create mode 100644 features/passwords.feature create mode 100644 features/session.feature create mode 100644 features/step_definitions/ad.rb create mode 100644 features/step_definitions/base.rb create mode 100644 features/step_definitions/bookmark.rb create mode 100644 features/step_definitions/comments.rb create mode 100644 features/step_definitions/mobile.rb create mode 100644 features/step_definitions/node.rb create mode 100644 features/step_definitions/page.rb create mode 100644 features/step_definitions/password.rb create mode 100644 features/step_definitions/plane.rb create mode 100644 features/step_definitions/session.rb create mode 100644 features/step_definitions/topic.rb create mode 100644 features/step_definitions/user.rb create mode 100644 features/support/env.rb create mode 100644 features/support/named_routes_fix.rb create mode 100644 features/topics.feature create mode 100644 features/users.feature create mode 100644 lib/active_support/cache/null_store.rb create mode 100644 lib/assets/.gitkeep create mode 100644 lib/assets/images/icon/location.png create mode 100644 lib/assets/images/icon/mobileme.png create mode 100644 lib/assets/images/icon/sina_weibo.png create mode 100644 lib/assets/images/icon/twitter.png create mode 100644 lib/assets/images/icon/tx_weibo.png create mode 100644 lib/rabel/active_cache.rb create mode 100644 lib/rabel/base.rb create mode 100644 lib/rabel/captcha.rb create mode 100644 lib/rabel/link_email_parser.rb create mode 100644 lib/rabel/model.rb create mode 100644 lib/tasks/.gitkeep create mode 100644 lib/tasks/cucumber.rake create mode 100644 log/.gitkeep create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/avatar/default.png create mode 100644 public/avatar/medium_default.png create mode 100644 public/avatar/mini_default.png create mode 100644 public/banner/default.gif create mode 100644 public/favicon.ico create mode 100644 public/robots.txt create mode 100755 script/cucumber create mode 100755 script/rails create mode 100644 spec/controllers/admin/advertisements_controller_spec.rb create mode 100644 spec/controllers/admin/cloud_files_controller_spec.rb create mode 100644 spec/controllers/admin/nodes_controller_spec.rb create mode 100644 spec/controllers/admin/notifications_controller_spec.rb create mode 100644 spec/controllers/admin/pages_controller_spec.rb create mode 100644 spec/controllers/admin/planes_controller_spec.rb create mode 100644 spec/controllers/admin/rewards_controller_spec.rb create mode 100644 spec/controllers/admin/site_settings_controller_spec.rb create mode 100644 spec/controllers/admin/topics_controller_spec.rb create mode 100644 spec/controllers/admin/users_controller_spec.rb create mode 100644 spec/controllers/admin/welcome_admin_controller_spec.rb create mode 100644 spec/controllers/bookmarks_controller_spec.rb create mode 100644 spec/controllers/comments_controller_spec.rb create mode 100644 spec/controllers/nodes_controller_spec.rb create mode 100644 spec/controllers/notifications_controller_spec.rb create mode 100644 spec/controllers/pages_controller_spec.rb create mode 100644 spec/controllers/registrations_controller_spec.rb create mode 100644 spec/controllers/sessions_controller_spec.rb create mode 100644 spec/controllers/topics_controller_spec.rb create mode 100644 spec/controllers/users_controller_spec.rb create mode 100644 spec/controllers/welcome_controller_spec.rb create mode 100644 spec/helpers/application_helper_spec.rb create mode 100644 spec/models/account_spec.rb create mode 100644 spec/models/advertisement_spec.rb create mode 100644 spec/models/bookmark_spec.rb create mode 100644 spec/models/cloud_file_spec.rb create mode 100644 spec/models/comment_spec.rb create mode 100644 spec/models/following_spec.rb create mode 100644 spec/models/node_spec.rb create mode 100644 spec/models/notification_spec.rb create mode 100644 spec/models/page_spec.rb create mode 100644 spec/models/plane_spec.rb create mode 100644 spec/models/reward_spec.rb create mode 100644 spec/models/topic_spec.rb create mode 100644 spec/models/user_spec.rb create mode 100644 spec/requests/users_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/controller_macros.rb create mode 100644 spec/support/controller_matchers.rb create mode 100644 test/factories/accounts.rb create mode 100644 test/factories/advertisements.rb create mode 100644 test/factories/bookmarks.rb create mode 100644 test/factories/comments.rb create mode 100644 test/factories/nodes.rb create mode 100644 test/factories/notifications.rb create mode 100644 test/factories/pages.rb create mode 100644 test/factories/planes.rb create mode 100644 test/factories/topics.rb create mode 100644 test/factories/users.rb create mode 100644 test/support/ads/linode.png create mode 100644 themes/images/ruby_china/bg.png create mode 100644 themes/stylesheets/ruby_china/i_theme.css.erb create mode 100755 vendor/assets/images/ui-bg_flat_75_ffffff_40x100.png create mode 100755 vendor/assets/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100755 vendor/assets/images/ui-bg_glass_65_ffffff_1x400.png create mode 100755 vendor/assets/images/ui-bg_glass_75_dadada_1x400.png create mode 100755 vendor/assets/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100755 vendor/assets/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100755 vendor/assets/images/ui-icons_222222_256x240.png create mode 100755 vendor/assets/images/ui-icons_454545_256x240.png create mode 100755 vendor/assets/javascripts/jquery_effects_core.js create mode 100755 vendor/assets/javascripts/jquery_effects_highlight.js create mode 100644 vendor/assets/javascripts/jquery_elastic.js create mode 100644 vendor/assets/javascripts/jquery_hotkeys.js create mode 100644 vendor/assets/javascripts/jquery_smooth_scroll.js create mode 100755 vendor/assets/javascripts/jquery_ui_core.js create mode 100755 vendor/assets/javascripts/jquery_ui_datepicker.js create mode 100755 vendor/assets/javascripts/jquery_ui_datepicker_cn.js create mode 100755 vendor/assets/javascripts/jquery_ui_mouse.js create mode 100755 vendor/assets/javascripts/jquery_ui_sortable.js create mode 100755 vendor/assets/javascripts/jquery_ui_widget.js create mode 100644 vendor/assets/stylesheets/.gitkeep create mode 100755 vendor/assets/stylesheets/jquery_ui.css.erb create mode 100644 vendor/plugins/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4c9137 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile ~/.gitignore_global + +# Ignore bundler config +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 + +# Ignore all logfiles and tempfiles. +/log/*.log +/tmp + +.DS_Store +.cache_rake_t +/config/database.yml +/config/settings.yml +/.sass-cache +/public/uploads/ +/public/assets/ +chromedriver.log +/vendor/bundle/ +/*.sh +.rvmrc diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..53607ea --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..013de42 --- /dev/null +++ b/Gemfile @@ -0,0 +1,70 @@ +source 'http://ruby.taobao.org' + +gem 'rails', '3.2.8' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' + +# gem 'pg' +gem 'mysql2' + + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + + gem 'therubyracer', '~> 0.10.2', :platforms => :ruby + gem 'uglifier', '>= 1.0.3' +end + +gem 'jquery-rails' + +# To use ActiveModel has_secure_password +# gem 'bcrypt-ruby', '~> 3.0.0' + +# Use unicorn as the web server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + +# To use debugger +# gem 'ruby-debug19', :require => 'ruby-debug' + +group :test, :development do + gem 'rspec-rails' + gem 'debugger' +end + +group :test do + gem 'shoulda-matchers' + gem 'factory_girl_rails', '~> 3.5.0' + gem 'cucumber-rails' + gem 'database_cleaner' + gem 'capybara', :git => 'https://github.com/jnicklas/capybara.git' +end + +group :development do + gem 'quiet_assets', '~> 1.0.1' + gem 'awesome_print' +end + +gem 'haml' +gem 'devise' +gem 'settingslogic' +gem 'cancan' +gem 'kaminari' +gem 'carrierwave', "~> 0.6.2" +gem 'rmagick' +gem 'mime-types' +gem 'redcarpet' +gem 'coderay' +gem 'kgio' +gem 'dalli' +gem 'acts_as_list' +gem 'rails-settings-cached', '= 0.2.1' +gem 'facebox-rails', '~> 0.1.2' +gem 'thin', '= 1.4.0' +gem 'default_value_for', '~> 2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d2ec4b3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,249 @@ +GIT + remote: https://github.com/jnicklas/capybara.git + revision: 3d158a0dc03276e2a79fe5f50501800b12358354 + specs: + capybara (2.0.0.beta2) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 1.0.0.beta1) + +GEM + remote: http://ruby.taobao.org/ + specs: + actionmailer (3.2.8) + actionpack (= 3.2.8) + mail (~> 2.4.4) + actionpack (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.4) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.3) + activemodel (3.2.8) + activesupport (= 3.2.8) + builder (~> 3.0.0) + activerecord (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activeresource (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + activesupport (3.2.8) + i18n (~> 0.6) + multi_json (~> 1.0) + acts_as_list (0.1.8) + addressable (2.3.2) + arel (3.0.2) + awesome_print (1.0.2) + bcrypt-ruby (3.0.1) + builder (3.0.0) + cancan (1.6.8) + carrierwave (0.6.2) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + childprocess (0.3.5) + ffi (~> 1.0, >= 1.0.6) + coderay (1.0.7) + coffee-rails (3.2.2) + coffee-script (>= 2.2.0) + railties (~> 3.2.0) + coffee-script (2.2.0) + coffee-script-source + execjs + coffee-script-source (1.3.3) + columnize (0.3.6) + cucumber (1.2.1) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.11.0) + json (>= 1.4.6) + cucumber-rails (1.3.0) + capybara (>= 1.1.2) + cucumber (>= 1.1.8) + nokogiri (>= 1.5.0) + daemons (1.1.9) + dalli (2.1.0) + database_cleaner (0.8.0) + debugger (1.2.0) + columnize (>= 0.3.1) + debugger-linecache (~> 1.1.1) + debugger-ruby_core_source (~> 1.1.3) + debugger-linecache (1.1.2) + debugger-ruby_core_source (>= 1.1.1) + debugger-ruby_core_source (1.1.3) + default_value_for (2.0.1) + devise (2.1.2) + bcrypt-ruby (~> 3.0) + orm_adapter (~> 0.1) + railties (~> 3.1) + warden (~> 1.2.1) + diff-lcs (1.1.3) + erubis (2.7.0) + eventmachine (0.12.10) + execjs (1.4.0) + multi_json (~> 1.0) + facebox-rails (0.1.2) + railties (~> 3.0) + thor (~> 0.14) + factory_girl (3.5.0) + activesupport (>= 3.0.0) + factory_girl_rails (3.5.0) + factory_girl (~> 3.5.0) + railties (>= 3.0.0) + ffi (1.1.5) + gherkin (2.11.1) + json (>= 1.4.6) + haml (3.1.6) + hike (1.2.1) + i18n (0.6.0) + journey (1.0.4) + jquery-rails (2.0.2) + railties (>= 3.2.0, < 5.0) + thor (~> 0.14) + json (1.7.4) + kaminari (0.13.0) + actionpack (>= 3.0.0) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + kgio (2.7.4) + libv8 (3.3.10.4) + libwebsocket (0.1.5) + addressable + mail (2.4.4) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.19) + multi_json (1.3.6) + mysql2 (0.3.11) + nokogiri (1.5.5) + orm_adapter (0.4.0) + polyglot (0.3.3) + quiet_assets (1.0.1) + railties (~> 3.1) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-ssl (1.3.2) + rack + rack-test (0.6.1) + rack (>= 1.0) + rails (3.2.8) + actionmailer (= 3.2.8) + actionpack (= 3.2.8) + activerecord (= 3.2.8) + activeresource (= 3.2.8) + activesupport (= 3.2.8) + bundler (~> 1.0) + railties (= 3.2.8) + rails-settings-cached (0.2.1) + rails (>= 3.0.0) + railties (3.2.8) + actionpack (= 3.2.8) + activesupport (= 3.2.8) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (>= 0.14.6, < 2.0) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + redcarpet (2.1.1) + rmagick (2.13.1) + rspec (2.11.0) + rspec-core (~> 2.11.0) + rspec-expectations (~> 2.11.0) + rspec-mocks (~> 2.11.0) + rspec-core (2.11.1) + rspec-expectations (2.11.2) + diff-lcs (~> 1.1.3) + rspec-mocks (2.11.1) + rspec-rails (2.11.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec (~> 2.11.0) + rubyzip (0.9.9) + sass (3.2.0) + sass-rails (3.2.5) + railties (~> 3.2.0) + sass (>= 3.1.10) + tilt (~> 1.3) + selenium-webdriver (2.25.0) + childprocess (>= 0.2.5) + libwebsocket (~> 0.1.3) + multi_json (~> 1.0) + rubyzip + settingslogic (2.0.8) + shoulda-matchers (1.2.0) + activesupport (>= 3.0.0) + sprockets (2.1.3) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + therubyracer (0.10.2) + libv8 (~> 3.3.10) + thin (1.4.0) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) + thor (0.15.4) + tilt (1.3.3) + treetop (1.4.10) + polyglot + polyglot (>= 0.3.1) + tzinfo (0.3.33) + uglifier (1.2.7) + execjs (>= 0.3.0) + multi_json (~> 1.3) + warden (1.2.1) + rack (>= 1.0) + xpath (1.0.0.beta1) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + acts_as_list + awesome_print + cancan + capybara! + carrierwave (~> 0.6.2) + coderay + coffee-rails (~> 3.2.1) + cucumber-rails + dalli + database_cleaner + debugger + default_value_for (~> 2.0.1) + devise + facebox-rails (~> 0.1.2) + factory_girl_rails (~> 3.5.0) + haml + jquery-rails + kaminari + kgio + mime-types + mysql2 + quiet_assets (~> 1.0.1) + rails (= 3.2.8) + rails-settings-cached (= 0.2.1) + redcarpet + rmagick + rspec-rails + sass-rails (~> 3.2.3) + settingslogic + shoulda-matchers + therubyracer (~> 0.10.2) + thin (= 1.4.0) + uglifier (>= 1.0.3) diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..324f8f0 --- /dev/null +++ b/License.txt @@ -0,0 +1,15 @@ +Rabel +----- +Copyright (c) 2011 ForkGeek, All rights reserved. + +Project Babel 2 +--------------- +Copyright (c) 2010, Xin Liu All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the OLIVIDA nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README b/README new file mode 100644 index 0000000..1fb8eb5 --- /dev/null +++ b/README @@ -0,0 +1,4 @@ +Rabel +======= + +Project Babel 2 on Rails. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..06f15ae --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Rabel::Application.load_tasks diff --git a/app/assets/images/admin/ads.png b/app/assets/images/admin/ads.png new file mode 100644 index 0000000000000000000000000000000000000000..fd6ce342b509973611d6ca5c560e1d28045f0c3e GIT binary patch literal 1319 zcmV+?1=#wDP)XVfGlVfVnkb7ngZ6QdPWxHKxx8)-o z3>R;T#=G$&p8Nxd;oyyvfrOwjA)YlxV}jvgu)f(3c2WG8WOjb@-fw>I&6}M8sDCKw zx*8M!N}5SCiKs9>F)1{>gCHD*MhHQpBpZ4pogM;gj0?ZD?Kco$<9hr4{=c@y95G~o zzyZYNoFSJG&j8eo$vQOwxHYtQPn$a8dd6v{m>}X~3>R!HGMu$>%;Jn>`VfyJJ}4KZ z9O5G4wye)y@L@X#t2HNxMyMhStQKin&MPYMrTY)}f3}iZapN7r-lUt+42~zUf6LSU zQ5?fqhtOPcf1%7Yn)^E^jlakfvy%SLrL?sDdeeJO|Y z;ifc@MjS%?LpRfoPOw!|Q$x0Xu$LI|Vb`8l`jd`5udCL7fcn9^w31-y6$Up+3I401l!AJBcAVV2h6u&O3fF9R-3o6KE zE5{(Z7^;Lll?8f3kyr11-Ltyvi&C(nkliQ=-`s$yU1mGzp2Eufa;vAY!7W{1Z(5m$ z2x_aAVhOuq=@>o*Uk-U~G2|(*49T6;b_y2SJgfLt{48#WkHu%=r$5xF@Jo#%zWB_u zU-kq!6WHAh_h+#<3XGyxfoUtY0TS9Ym_@$6=Paz%+;OgxOLE;@4+z{P?lRZQ#c+0k z8*&+ZoR9Gl5ct!458ufTFzoJxKZU*?{Kog3fb83={{)GTMSK-$hpbieDQk7r@)`IA zlgyaxQuUSfS*jF@rqCsd=Y$sJ%-m*Z; zs%JB=09^cn|L~e;o2&r5FaVC6^=x6hQ->b`ES-}pwCddNK;Siikw4RA&zhq6=De@f zw(x#5JOJ3aU#oq8QmgGeMg2{HSL#oxBMR0tLK+bO0007FOGiWi|A&vvzW@LL32;bR za{vGf6951U69E94oEQKA0cJ@=K~yNueUr~_K|vJ8KW-Zn#oo$BP+Kn`SdvI|AtJOM zqTPymf~u!zYa_9gknjXIO8j{QvE=5uU(dZPYi2WZ&Mz}_=6v6P?l{}zwpY!>`zQ{(l06rl1rC@XD*_+ zM}0%bJ<8#xE@5;b01o$Q7@;2zS(b+fAp?(lIXoFK2 z_~Mb>0Hhor0$mBdB{`8hD^eUSHlz$F1(_NWJ_8kE6@gli?@-7Mce`Ou79Db-5LuxI zl*o0+k5{)Qhi~vdtjQ7t)qoGO0~BaG$D?Dj0C%fwsv!f6$viM1N3qz58~}N83_)@D zOD;)dUw zHqjeQZ%w_aZ+?eAVRfQIrMGro0000bbVXQnWMOn=I%9HWVRU5xGB7bREif@HFfvp! zGdeUhIxsOSFgQ9eFoHg7v;Y7AC3HntbYx+4WjbwdWNBu305UK!GA%GMEif`vGBY|f dG&(RbD=;`ZFfj9D(+B_n002ovPDHLkV1o4xNsIsh literal 0 HcmV?d00001 diff --git a/app/assets/images/admin/bg.png b/app/assets/images/admin/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..72fe1377ec07900966a2107e959597b495f17242 GIT binary patch literal 12875 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/admin/nodes.png b/app/assets/images/admin/nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..600ecfad2903770e80dcc378b44574423959e4ce GIT binary patch literal 1374 zcmV-k1)=(hP)XVfGlVfVnkb7ngZ6QdPWxHKxx8)-o z3>R;T#=G$&p8Nxd;oyyvfrOwjA)YlxV}jvgu)f(3c2WG8WOjb@-fw>I&6}M8sDCKw zx*8M!N}5SCiKs9>F)1{>gCHD*MhHQpBpZ4pogM;gj0?ZD?Kco$<9hr4{=c@y95G~o zzyZYNoFSJG&j8eo$vQOwxHYtQPn$a8dd6v{m>}X~3>R!HGMu$>%;Jn>`VfyJJ}4KZ z9O5G4wye)y@L@X#t2HNxMyMhStQKin&MPYMrTY)}f3}iZapN7r-lUt+42~zUf6LSU zQ5?fqhtOPcf1%7Yn)^E^jlakfvy%SLrL?sDdeeJO|Y z;ifc@MjS%?LpRfoPOw!|Q$x0Xu$LI|Vb`8l`jd`5udCL7fcn9^w31-y6$Up+3I401l!AJBcAVV2h6u&O3fF9R-3o6KE zE5{(Z7^;Lll?8f3kyr11-Ltyvi&C(nkliQ=-`s$yU1mGzp2Eufa;vAY!7W{1Z(5m$ z2x_aAVhOuq=@>o*Uk-U~G2|(*49T6;b_y2SJgfLt{48#WkHu%=r$5xF@Jo#%zWB_u zU-kq!6WHAh_h+#<3XGyxfoUtY0TS9Ym_@$6=Paz%+;OgxOLE;@4+z{P?lRZQ#c+0k z8*&+ZoR9Gl5ct!458ufTFzoJxKZU*?{Kog3fb83={{)GTMSK-$hpbieDQk7r@)`IA zlgyaxQuUSfS*jF@rqCsd=Y$sJ%-m*Z; zs%JB=09^cn|L~e;o2&r5FaVC6^=x6hQ->b`ES-}pwCddNK;Siikw4RA&zhq6=De@f zw(x#5JOJ3aU#oq8QmgGeMg2{HSL#oxBMR0tLK+bO0007FOGiWi|A&vvzW@LL32;bR za{vGf6951U69E94oEQKA0i8)iK~yNuZIi!C96=n#pDP#@iHQ+JND#zJ@Hjh#YZTPe z`~f!C*jd>Gix2`KsZv>_vC+fILW6|}3WAM4(34mQY9lss7RKZvK`uGmnce4i_LSS% zyJ4APKC|!p-uJr;lAfZkd{!<93(h^i8uWNRk?8&UkG@$p^mx>Q8VAF7taw-8Tcppn z6ZpoWa#ck3>Pc&!TtjnLXRLi(s)S(;RsAE`$;Zq?S5bam_(v9a5@H5$vWl zss|tc{OHCUl?{#d3F4mSccjanqHw7iEXn3YHY0`~2Ji^wx)^E*44E|A4?__)QN9%9 zn)+D?=39nhGAr$q!7Rk<+TKa>Fx1>NkTY;z)FUOmvEUvCeqElnVV=itut;UW9XMug zgS_J`CN1dk@Kvm{s{=_XKaD91N?6x*z1tW*Tr9U@gW7^6@m0rsa1*xM(g0+12V>p+ znMlrZ?O?ACskQk-WZeS~<$wgN3a#$|001R)MObuXVRU6WV{&C-bY%cCFflSMFflDK zGE_1%Ix{vpGB7JJI65#eOQNE~0000bbVXQnWMOn=I&E)cX=Zr00RR9107*qoM6N<$f&jU3M*si- literal 0 HcmV?d00001 diff --git a/app/assets/images/admin/pages.png b/app/assets/images/admin/pages.png new file mode 100644 index 0000000000000000000000000000000000000000..2ed77eb202bf49b90e870083c170b5e24f8e2a9b GIT binary patch literal 1280 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/admin/reward_history.png b/app/assets/images/admin/reward_history.png new file mode 100644 index 0000000000000000000000000000000000000000..32d2f0c65097011d0c3b681e7646148c21d5fba6 GIT binary patch literal 1890 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 ZD=;`ZFfbD$k(K}e002ovPDHLkV1m%$RIC61 literal 0 HcmV?d00001 diff --git a/app/assets/images/admin/users.png b/app/assets/images/admin/users.png new file mode 100644 index 0000000000000000000000000000000000000000..614779e8ac35972f6dedb5adec492df8781464ad GIT binary patch literal 1444 zcmV;V1zY-wP)XVfGlVfVnkb7ngZ6QdPWxHKxx8)-o z3>R;T#=G$&p8Nxd;oyyvfrOwjA)YlxV}jvgu)f(3c2WG8WOjb@-fw>I&6}M8sDCKw zx*8M!N}5SCiKs9>F)1{>gCHD*MhHQpBpZ4pogM;gj0?ZD?Kco$<9hr4{=c@y95G~o zzyZYNoFSJG&j8eo$vQOwxHYtQPn$a8dd6v{m>}X~3>R!HGMu$>%;Jn>`VfyJJ}4KZ z9O5G4wye)y@L@X#t2HNxMyMhStQKin&MPYMrTY)}f3}iZapN7r-lUt+42~zUf6LSU zQ5?fqhtOPcf1%7Yn)^E^jlakfvy%SLrL?sDdeeJO|Y z;ifc@MjS%?LpRfoPOw!|Q$x0Xu$LI|Vb`8l`jd`5udCL7fcn9^w31-y6$Up+3I401l!AJBcAVV2h6u&O3fF9R-3o6KE zE5{(Z7^;Lll?8f3kyr11-Ltyvi&C(nkliQ=-`s$yU1mGzp2Eufa;vAY!7W{1Z(5m$ z2x_aAVhOuq=@>o*Uk-U~G2|(*49T6;b_y2SJgfLt{48#WkHu%=r$5xF@Jo#%zWB_u zU-kq!6WHAh_h+#<3XGyxfoUtY0TS9Ym_@$6=Paz%+;OgxOLE;@4+z{P?lRZQ#c+0k z8*&+ZoR9Gl5ct!458ufTFzoJxKZU*?{Kog3fb83={{)GTMSK-$hpbieDQk7r@)`IA zlgyaxQuUSfS*jF@rqCsd=Y$sJ%-m*Z; zs%JB=09^cn|L~e;o2&r5FaVC6^=x6hQ->b`ES-}pwCddNK;Siikw4RA&zhq6=De@f zw(x#5JOJ3aU#oq8QmgGeMg2{HSL#oxBMR0tLK+bO0007FOGiWi|A&vvzW@LL32;bR za{vGf6951U69E94oEQKA0pm$TK~yNueUd+D6hRcm-|XE*kzf=XZBqINEVPm!hzOEC zQ4|Ynn^f-JW|Q09nY+^@R?%Rgg-8&D(?T%Csf`h06A>j>1T+S*@Xy5vLX_lY_Vdjq zqL(}UW(H>X&3kXY;Sv5L`umU=^G~P~=DIyJ_&L9zk+g#bliiuzp=U5nFfFC=fCP{2 z^Gr92r;sPF9?A*PK^M~6X?6>LysE7xSlGgG)tVV%7B=6eZkUb z-h0BaWKi=;DtLz~Wm3591FCL0ytIOZ;k2)m{}R(SE3ch(+#OVL^eKfid4_X%-??7% zFmTUTaa>Q~;*gBw5^&t+&5{c|&liWsb^0KEy%0=d4YwHza$W<=W$8a=yTo=Y!Qdk_ z@Nox;3>~pQG*JlVU<_epW-JP?)z*N6Y`Jsx#uzbZmvD;dqj{WCKV=|1S417{DibpOF9POmv65aj7qa_Yj42#j( zeQd&U)|tZzx{eK$rBaj+F7B}o1pNuM`;*f1sWR0=8HbuB_-=8kLa6T&xonHJl7|N2 ztsp@yvW8a)pHKDm_qFJB?*w#H-?nLL-Bt~=spB{67q2YZD^2|*DgXcgC3HntbYx+4 zWjbSWWnpw>05UK!GA%GMEif`vGBY|eI65>iD=;`ZFffw@oA&?!03~!qSaf7zbY(hi yZ)9m^c>ppnF)}SMF)c7MR5CLl~@&51D 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 MPgg&ebxsLQ04>#0TmS$7 literal 0 HcmV?d00001 diff --git a/app/assets/images/bg_top_light.png b/app/assets/images/bg_top_light.png new file mode 100644 index 0000000000000000000000000000000000000000..8cdcab9dbe022fdf9fb134d411041b3c0e64217b GIT binary patch literal 3568 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 u7JdjF_|S9uvGhc)B=-RNPAX^Z&m+vuZ!t`Q3n zS0Q1xcNKfy_)U+v(dquzFNr6DkJo@rr*X%e0-XSvUS)lK1{Mhho>oQSrJRq1f%YZ@U!b*x!8$U23$ zIz3!*G2>2umSvQa%<<~9GPj~@clTd5Xu4-m0`jc zmrd*m2k&mbWKt_pRb;CWIsHn}cVEwHhF=W7(ze-4@SK_yrSjU|c5y@3lDtVOu?)GM zdYZ~N^UP+;GRUiF3wy5!TdnkRk{;s}k7>@F+Zp0K-g#|O@wE8zXI7Jn@n%_ug71zQ zkE$&CPaW^ScvsNKlH)?LXY&0s5e^1{mo639droJbU@5y;!-}uL^Pi2wCF_Q8hT=(9 z3~7h1s-IM95Nr6k-^ilyyY!@N6?xBTOh0%JY+7P%eEUS@zrW^njK4fLIxT$Iz6g|T NJzf1=);T3K0RW9(7d-#~ literal 0 HcmV?d00001 diff --git a/app/assets/images/heart-gray.png b/app/assets/images/heart-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..7591e350bcd5ac2895a341850d3ad8f85a797aca GIT binary patch literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wk0|S#> zW=KRygs+cPa(=E}VoH8es$NBI0Z=sqgH44MkeQoWlBiITo0C^;Rbi_HR$&EXgM{^! z6u?SKvTcX`(q39jqUs2QK}=FxMWr?7fY-}*<~ zM|@p4Z*n-sxZG)RiPoQQcdoUCnacd{gHv!}aUe**8vBS$H$&)0Zzt zUcS`47L~$ia7^I%$BVm<7xoyo9a5iLT=728Yh&E{ex_@$O&tXu>^*L|ot?oY@JNDz z1=kE8wU$JV4eS-~PdoJ(eyQ49Kdb$)V)xOeq?O<6?mK!;p7i{A`mx6!_i zvZKHhhU~X?jZ=i4%bc@pQdn^N?WOnMPi|Usr>4HXKWNpQ$wBXS7oSsXS>O?rVIpNX z)2Cxm$M^5$OQx_JJ8{e@{@uT1s~V5~F{U%e2u`EFj|S1;B2+FH@+ z)2GjwKVQC zW=KRygs+cPa(=E}VoH8es$NBI0Z=sqgH44MkeQoWlBiITo0C^;Rbi_HR$&EXgM{^! z6u?SKvTcDi`-VboNT*#Hg41An>S~^H!@4~6}V~p^r6l9N`Cj6 zU1h4dW{-+4yyfR&nS0I8^w-t9^CgQOFXQsRzr$|E&H_P2D{&LMJ=R}R)3!a@rB~bc ze!`y0n}$3JzxggmS1{)Zc)U3#CDH9Ga&+qYikrzZ3{;F?Fbk~97fji_;t#*uv2zc8 z$c1>HIB@i8=OH&ihSsU?W%d?N_t9f_sLSFKzGkuW2S-Cj>D2k#^NK3oh%i_M?ug2~ zq_S+^&Z8%OxNx(b@bi1uJ@@2O3CZ~iHCFELCnj#WWMOsjhRQ*c*({51ZrjejIMQcx zr>{`SqQV}dh^2+h{GmEqL{}s+zVuvkNg}nNXPd%-_n%grIloo(O1%B?_n#aZE{eHl zb2}wGmatzJSYz-ve#b5Y|6SX>R*7&l=5fk6N~wD&RDEnIsC+4J5fhj6`(drk%L&Ig zEp{_4)_r4k+2WvZz}%UOOssqFax&hCo3Xs;z1cqJY`&KXTaKElv1)GIn3Y@HaBTB& zm-S+TwtH{fXI&z2pUexcevbL#nJ6m23M&+LzZv a{A1Lv5J+q0{QMDAYIwT(xvX!C1%w79Y18Ee3#GNyKE^0K#aOQ7$o!ub*!)9s`+{eA+qf=^5sD zVYxApQGmptJI{dWHx)*)P@zI_zm|d%7TMA z+t(JuM01uZ6cqt-_;h=BL}bs)l_Uz4*nNF*ke0;;EnkUk<1r_8G%mS%y;@%)&rYXO zWq;)~`M7e}9wor=hfCI#mr_HD_W#B$%CZXsW6rrUa$$Bz-bZM_^$xVivuJO-jScR+ zpKxn0Bfp=jr^8r9(6P9iP9j>ONT!meemFd()T(f*xt=;K^6aMi!~&MYt5#Ht=(Yfl zi-+0~d9!rU+cxnapkh4Tx0C)k_SZ7oeO_#3jiOdW+=bUp+l5>(AMbeOyWDzic1PO|gBq<6g0*Z(d zL?sC#K@d8Gmiy$=8^g`Qzy!7vQ~ zA)!$bHb%NUPR=eoxG8`IG=KzzfvP7tGEB$H${hT!ZEYMtdxK+=dvpD-ZvXQDt+!7k z834!%8TRsyB!?jQ4uY4+VG&UPz?BGodN?W!!6@Vc3=zlz5qxS7`~8DU_psMLxc;|~ zosAyi(+vQgmjJ-hg?a~s0zijGuqN5x(;EO5eZ)s1B;?Q@ z4nwer*T3fR`xo}~^!huer>Eav{Lg;zKOizJ*z?HmFaMWaNN_arynZ)2q$h<2 z@89DFk6iQKp895ga9Hqf&VJi{LhWt;*h9VcTmHem5r#H@a9EV?zp$0vAA6j?-u^$> zC(_{WJb|93R)6df(KhyfaAb_F!5u-?Y8+_0M8gY<`~` zh-vsgwn*festqD%VDvvWj|2c(T#+rV{2v=X0ik{pfCv2K=!lqq=0mnJ02n|3)PMo7 z0xrN0M1Ula14=+0XafUa3M_#ga0YIG4E#X|hyaH{0!Rd@AQPMg`Jfn-feLU9+yD)r z8Qce*;1L)APr-BW5=?^+;4@ePYhVk4ARI&q(L<~dHzWv&LvoM`qzM^7=8z5K40%9) zP$(1)B|s;kOehB`hAuhHiPZp zgRmbQ4#&aC@EN!eE{CtfO>ieX06&MP;m`0Y3ZRH6CKNA95~YIDLs_C+P`;=L)G<^B zssL4vszbG+dQoGjY1AU>CmM^UNAsX1(duYpv^|=P4o4qDXQ7MH*U(MqZuA&>2EBy- zg`vc7V8k(M7!!;W#t(BClZq+8T*WkEdN9v1A24fJES3o?j8(=OW1X>q*m!If_5$`M zwgWqYoyD%=a5z?+I8Fm+iSxuo;Zku$xa+uf+$in?ZUaxmbK~XkhIkizC_V{afUm)~ z;Yaav_$>+=3PB1r3QG!aiX#-+6qOXM6vGrBD7FZ+1Yv>(!Hy6}NF)>z>IglASA-QJ zk;q3>C0Y{$h$o1J#Cqam;tcU8iH;;j(j&Q&qDf~+RiqBm3(^WDC8ZFhHl+(?6y+Jp zYRZR{ZzwmY=&7WrOsKr6j#HITHBpUHeWj+L7NpjpcB77?E}*_mJxskwLqQ`%qetUO zbBv~h<}S?y%_=QDtt_o2Z3t}^Z4GT7?K~ZhPKeHc&YLcou7d6%-7Gzdo}XTio=ks| zzLLI&{sRM+L72gWA&?=H;ReGH!xAGMqXMG?V=Q9{;{(QNCKQtplL=EWQ#Mlr(>T*6 zGbghSvk!ARa~<<2^EwMVi#Cf7O9snLmgg*+tURm+tU;{jSesd2v!U6<*sR$Ovt4BC zVOwNpU{_}+vuCh3uupNo9AX?c9Pu0#9D^LIoLrnnoMD_roDVq{xtO@LxdOTJxZ1cD zxaqkyxdXWKxZAlu@i6k}@C5S|@^teo@pAAQ^G5St<{jeQ;uGex=S$+d$@iL{z^}sZ z%b&;J#lIxLEnp##AW$nXB}gHtBIqx8Ua(hiT}W8SQ7BEQS!iCEMc70*PPkV1l?bJX zmPoirxyUn7tf-1;kmv=`r(&>}qL{x}iP*3>EUqLTAbvr7L;@qBDiJDCE-@iVlGKrm zk*t-Rkz$fEmr9aqmij6!AnhW3PP$k6myDuJuuP@QOIbQuQ`schR@r4aF*&kaiQF@J zN_j*1 z#i;749#_4u`cq9wElRCH?VGx!dXRdJ`uslOeLnjt_sweXX?SW})|l4h(e%)~q&cm{ ztL3RxuJulvU)x*zs`i|Ys7|2Hb)B!eGP)7Ejk@c4YI;ZYI`mQcM*120Lk4sP_6EfU zZw&bi{S0djmy8sR;*C0tF~(-b=Zq&zI8DeV)h0`(%BDw6yUj>uwr0g<@65%_Bg|VZ zU<*@=T#Hxx1^0*UZ?*)MCYHIDudRfw!maLEW38>MORPWI$l4^>^xD$fy4%*+uG#6@ zW!t^97qO4F?{c7aaCNA5Sa;NS%yFD{l6E@kG~mqU9N^sIf^%_jx$3fdz~Dgsfe)^V zuBoo$2ZayD9_({ta|?ER;7;xC>3-V-Sf|p>a|4HBcCUK_SW{! z^Iq`L@X7I+^VRUp@tyb6@XPgE@YnJ$@Lvqj3n&iw9%vj`7PuZ{8B`Ut8|)Z-GXx*v z5pp+_E;J~#`w-Wm*h3>>;$f*_)8T62`Qb|uW)W8-p~!=gEl~_eA~P5*5}gt~6QdbZ z60>pG;qdKP+St(8!6RZv(vQr=8OB|SN5_-nyApU45))>Q>Kwg%3_9j@tm`=c@#Nzl zP8glImPkkpN_>(ela!P6B4_oVmOWi~diRX)nW1c@?2BiyXG719pVK;bEr&KIE@w8^BDX1z zCod!KN4|T0Ux7kF*?GeG==0Ns=7lXq0!3$wwu}9XpO@&C+$`lNO)p));C*5AqV~nQ zGS0Hhvdv2YmnJV8U2ZBDDle$OR>V|%taPaCy`plZ`YPMi%&R+9p;gn@Y_4@zD_2+7 zaMWbi!nM)03)fw*KfR%Uqoq!=?($8ho0;{XKDz$XEstB{4Q35pw^eS}-4VP~+DPA+ z(F8ReXJ z&UU$Xy?p5O@L9KY_fU^n&*Mjij~@2w^tM0NcznN4t?zEXN`Lc!(m>Oo;$Y(w#V3tJ zN<&S<%EK*BRiCzw>>Ftt)f(*_(;s{E%;ed?^Zn09#_h)^CtN3{C%q@y z_433k%&UyoG_MQZaJ(shEBf~4w9@p08U2~Tced|d&XQ+8zmI&s{UP}y<;Q|K?z!rD zx%vAGh6_(W9r*O&bJ*vt#ndmfUrN7)6$3U5#M*0vsPGFs(vW^=vuW} zeX|y_wzZzI!Mbtnr^?UXO~=i-t;5^6?ZO?=oz`Dwzh3Qz?CyqnMtJ_t1yD#2`}zW~ zTm=B7GXV5Y0U&hzlmGtqK+LET|)7B&;W*CaNeVBF-g2D~Xm| zmztNJk{Oh3murx(P{>hCQVLT(sA8#Qrz}&fPFkO|Nwz&{mu#QxkmQ)?blf@KCH6q1Yv{oMH*a^c zhmU8FS2Q`%`;1SCZ=XB(E7I&3Btn9Aose5#* zxAAdH-@X0^18svHPdbM>hufdt8)+Q9K34wh{PV2w#EF>6z!wLntY7NCQhqJ=hT|>e z+h5aPW~Sct%-(um@*(Bpp*iPy-39Sa44-yBe_VX>rSa>9Zz)Tm-<_6qS0sP1u3}b~ z)?TeY+PL+zbTf4;Y};i=@0a8*)9&tH{&k@dI1Ock7R0b&rEtdhP>S0`nB+y-MlD2> zOGltjXGAl_GjFo`vc2HYHKsHb&NIFs zPqANlKy^_4$-W_tVa=!7Bf6soW5&zB({3Vvj*Ca%S-2W@!# zwBIz{(%hEa5&XrvOZsp8$D&lwL<~~;aawa#aew2L;dAFdE|4!+DO88>e_OOstWmsCqCxVeRGsv7nQO9@a+l;w6wWK= zC}k+0R7p^cQ1exH*=M1lt*L;p&#%L!%b~}q&uPG8$YaD~%w@u9%4)`BPGdpZkF$g= zcdfRqw`?|Tx9zs=w;i?|e>$x>FS~p_u;BXs;9Iws?vozlo)ca#$gjO;d}e(=_|5sx z2P_0G1kDG}h0KM{ANmru76C`nMF~VJ#h4sE5F2tNAucPvG@<5bGZIbvP7EaWC3T;? zm)ww2m0Fs1_EbuGY(`M#!7R(uI%njwh0k)FW5}V*rO3nPqY6O5&iS8(t3^x2pG!WJ zPG6Y1I94`%Y4CD?d4I(~<=~Z}t3y@8*M_TyYo63Txjt~Czpn3QUw!|rCk-RFpWm5k zeA_hJJlFE+?w8i3d&~EKJXmd8Z(r|N@7(Cxc(~EM(X;+&t#|eDO5gYXZv$TjKRx+4 zG&?-~^wr4Z=-AlMv&YZ7#_vxwPS(Asnksu)_$vE#+MArbZ zT%4Pn?_Q|?RPs4>G5m|`SEFwVOZ?xNm#J5XKM1RYHNrY!gYc7L6TgMq#_nK#p?0A? z{{PWI_F@8734p6ejc(rvfH_iwUNihN^l!I1Y_VmScUKqJ0uP1Ar;~wC=I#PJ%GJ|gK$o`TX-sbFn)+ao}!q5 zA{-*TBpMMLNSvf}(i)`?D%^g}L+Iw`mbOZGE^wSKX48Iu9G6^!> zXLev-VaZ`tV12=Mf?bq-lp~&#pL2jKikpkOpXUg#81HMoTz+!_oItl=vXFr=PPk7b zQ`AC?Mr=&HNWxQ6LUKduv2>A4h^)RGr`)FeD}{%O^-AT+MJjo!IcmA;=l5OEsM5Tn z)vZ0Dv#5tg_O`O2jZvs^rb&(IpxJi|*8TdH5mpt}6Sh=##`Z}L4;>GZH|+QuR1Dltn!3l z;|tTf6S}wvBbnc5U>~_Dc3y41_-^8txhS@l0ggeX?k3{58W{ zo0**1v5(C2?w@KFH@{hYuUy$(bKB_H65cuY`~86dlnDRaKorOWO+;gG0AAn_qAO&8 zB1B7Q0gu58@Cj@~)DS77mY&e zpp(!&7$U|7Q;M0xDq>Tx6F3RnN!)9^D!vfEN#RcMh#*5KB@&3S#E&Eg(j!V$$~r1R zsxoR0>Jl1GnsQoE+B8kPzq#*~E|dw9)sbVB z`yoG~(4cr;>4fqj6|$ybzCatY=hWLPXyciVaFuE>x7+U&fa|sEoQQS+!7IT^n>mrf#FYyCLU}cawSx^WE)xix1}8 zzjvX!IUcD!cI{6btR0#hA&hA~kDa*lV)>Qi8}I3YcRlZy=Xe%uKBsmB|b^0dGWEIu6c&Qg9tnkp{tQ@C||?Mo1J9OwE8`vG3748ad z8}ER>O+iNyMKMXxBwQg<5@U$dBr{SAqI?um5vh`>HmPH%S7@SW)@bADV7d%?2Kq7v z35HfiGsd?}(acoLH7v#~3#_NvhBO-WTuZ<=-C29h4aSKGgWo^>CVq!;x>JO=DVOMUPyFXGl16 zjCedVkt!)KnKPv(P4!fNhI{7P>2ujqXNPkl^Vssc&xaTB7C$e|yr_Q(y4+cja>cxg z`P%!Mrt2AXUiBIc9JhBGXPXD_w%)t(psM{^=dFkBJtMvI{is3SA-$&oqq)yI#=pN1 zeChH!^KH}2jEsyd2lwx&+`Vmm5lM~$1rZ?(G>~QQ z&Q0x~KD_<^=GC*m>({O*0E#gZ(gzNHVic`gv#9XJvj@LEzJK%o^T&7pAKt(7VbPq4 zQN-va==%9{rzSjo{NNkZ^&lVo|MKbG{|EPOz3pwQ@gigiUJIIP3vGd}eew14`~PoV zKS#mezI^zgF3SUfKglwXi70jFh{z=8!cRV_>{uPCdkvi`)WVoEATpi6%J;$i^m z73N@M;e;6uqERe(bo2fTW@g6uPoF;4`v33$KlJctLJ3faF`!TcyMmE{>E)}ZM~|M^ z|12~n2_gw3QGy603UoDJsGoz_@86#hj%ViwYhVYHsKieom;L8QhVQKZ88>Y@_Y4^E z-;hcH$Qn>AU}t9&0Q!`Hg&8PJn)%>}2DyouokbX@1#jQHW%&B#3&W3JKY-?w zZo{wNzZhP>eh$n)2!P^)r%xVUl9QJ=gG2x<5fNt*NYzJR?tlLD*)fFiXvs9v-Couy zC@@n(Ttfc;e6ZSS3W`|aCLFeNC-6edVb g8cW`!rtu6601#!DSv)s~4gdfE07*qoM6N<$f+RxD#sB~S literal 0 HcmV?d00001 diff --git a/app/assets/images/mobile/eject48.png b/app/assets/images/mobile/eject48.png new file mode 100644 index 0000000000000000000000000000000000000000..2cf35d6ac4263e97af2b5860260cb25d14e78f48 GIT binary patch literal 7490 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 M07*qoM6N<$f)8emv;Y7A literal 0 HcmV?d00001 diff --git a/app/assets/images/mobile/gear.png b/app/assets/images/mobile/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..3171a00fdb6d5c36e66ac4c4eeb00c2f6b3188de GIT binary patch literal 7099 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/mobile/gear48.png b/app/assets/images/mobile/gear48.png new file mode 100644 index 0000000000000000000000000000000000000000..cf268462d92a9e2b024cb80eb6fc35f1a42841a7 GIT binary patch literal 9627 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 Rrey#C002ovPDHLkV1lpYsnq}g literal 0 HcmV?d00001 diff --git a/app/assets/images/qbar.png b/app/assets/images/qbar.png new file mode 100644 index 0000000000000000000000000000000000000000..5f6a9a87e309b7a02d00e54b28a8288722171ea3 GIT binary patch literal 2295 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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/app/assets/images/reply_button.png b/app/assets/images/reply_button.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba4ef6870a4e0ecf69abbce1ad5f6a7e9c3782d GIT binary patch literal 1461 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy_!3HG7B;uuklw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6H#24v4 zq}24xJX@vryZ0+8WTx0Eg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i={n4aiL` zNmQuF&B-gas<2f8n`;GRgM{^!6u?SKvTcwn`Gtf;oFf&jvGt@IQ zHZeCh*HJJsFf`CNFw!?P(ls=ndS0-B(gnVDi`;OJ^@Ze(g;X<=YuXz1c*2^4TNFmrQpG&M7J zb+Ulzb;(aI%}vcKf$2>_=rzQt7nBro3xGDeq!wkCrKY$Q<>xAZy=;|<#Vr=j7A~#^ zu1=QbZm!tf0@0g-#Vt_1&N%hz10ACeiddwGh6w>v4~Pj*wm=R%;iu*SQ+p9GSsz-r zLWhBYNz>ECF{I+wlqtR*(vC7m)6HkU_bu~u5@7K@n9bib-7DX=wJAW5MODG2D0h8p zoX!`GFS8bOim$(?BDXh6!#47RO2wy1@AWsGRNH6%rcEU)iM{R2xikNs?fpK#chc#n zNA@h!;HnS1_}+c-MAO~#&ZaG&ch%Kk{q^1Q94!B`W{H(ND4zZ8Qq!tcY!1gKdZb7v zU(7H$l3?(oULn-sY!zV>2< zms?(Z{;6^%m*s1fZRXs$kMp*lu1k!(=BPHgQX<+djxD6N^Y~+{HCz$%nk-o-byZ4y zKaylP(MOFn>1WH%*GU_HLK;wmw2lv{r z)9bEg-M5~5&i%<@ZX4D^a}Lb9nPVpUCe$FV`Re+Hc&0SQJzVVWp91PMZDxjLa#~8A z*vxTse(csWt7G(LGhCgUts44C_KWHn*@yGqg{;16zUfrRkp(MR)UW^k`%mnFWXzlR zfQ7~L0#-;zr$6QNTYg#RMBK`dU9BBw3xBmRsl+!)1+EP7Gl+9nsaI2eFeCJNoZO>r zAzELV+x2aK2JM(BJyZ9}y+x~}XV}d-`*kOqy7>9J)9a!{8WXs$?fY06*xXRB|Ibq3 n)g6YJ;#2j5I{)5vXJlhAVHJy;^SWmxs37%p^>bP0l+XkKACMmO literal 0 HcmV?d00001 diff --git a/app/assets/images/rss.png b/app/assets/images/rss.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf19534f18af1d9b8d212dcf801265242cfc24b GIT binary patch literal 747 zcmV;k`kqmsD!m}(V|=g;liNbg;9&3qJ^|*5wv&H zq919~szsSas}=CH4AJK^%(od6|U5`d5h7iO^d>V?NG8!icR z@IO34lv^_INrHja+92%Tg2YOJ)HT6&;Bv+##L_IHRKx00jAqHVn_V*9nnG0c zRRv(pJ)r4!Scgu7Htec6Dm}Rj?u1oNmNR)!sU0Xq8AM2)x(~LeAF{BB!kyzNzkN_~ zWDng%^s7|mLbLHNsHy7WZm+SP92$jlehk6%ORP`as)(?|S0Q{2M&`UQ0vcwP9u3Ox z9wMBdGJqDVz4TZOcuwt)nLRBo6FU?f8Ah|Q17d;I2*~L%_|LDSG(MuHTm-S}U=Q3t zF!dC2HETo?2Zv+FirI8Pl>?pvKXM!P$$KElI{0r!W9v-^V09iv_~~{0+|mun>kYH% zKno<(;2i^?jr-MC&O&BqAbw_IsI?C$ESR3Ao0YQ-fp~~gCC*Y|;gH3pjj-o|oTs24 zF|^GE*0PoBqjs%f3_E+VUAFXfgB9jMx3{Sn+Yrm1sxRPe{ePuTB{JC%_oYzU~d9I@?LBfdyja>Z43Iy{4 d$p?Q6FaXxHG;b{`bBF){002ovPDHLkV1n~pPgwu} literal 0 HcmV?d00001 diff --git a/app/assets/images/shadow.png b/app/assets/images/shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..0815ab64a221e4bde94d51564698780cc459d2de GIT binary patch literal 1009 zcmaJ=O=#0l9FGjTIjoAF<_A174}vzy>-w?8)!8+z4VqEAqCI$6n!K%{OJ0_|+H@#x zQ;?w-ZyW3+c-=ufOg*SaK~&g5kR3dEQRcy$p!m|Y)x&Dw<-Pak|NH&Fes85Pc`-bA zY>;7?u#%UHbUz)efe`&a_`PY-ZHQ#cWD3uahUOwBZQu$5ilxn>BGQcJ{2MgJFny=Y zQkj(1%aV>QP78cE-*PCLVaCRNN7HMFfC`#5?G*d{#Y+~LMvA=_R|VC{psJZ)aM9Gl zWJzDB>7v1op9f>UL1w#6&ab71CK&~hQQ6uX;LSuKDJb`gkkLPQr5 z5F}z8j0s{)JPV=%6nGfrg=7Q@QamXM5VSv*MstlBsVL{#vFI(uRta$=o^Lc7Tw{d8 z?ko>QQRIavAB{$+N5pH|MDruIH{4;6k*B++LriRgfKjX9I!UoK)7=s*r%P*l?KaVd z@xJEpkQ0KEI*zLPe^<-udV8dZ{^q+Uu~%w3h%X`!*Ik_sZe}z9IY^ryc3fup=vOyn(uE^<<|LNP- z>+qs*AakXEb>-K`M^EqWe7Jx7)}?3TqLP1()@$#+9=y4|6rQe6_iqlI*m?E-YBUG}J J^2WsU@*gI3K)L_` literal 0 HcmV?d00001 diff --git a/app/assets/images/star.png b/app/assets/images/star.png new file mode 100644 index 0000000000000000000000000000000000000000..3609d073879cf4353fa089aeb4620e3e882709f6 GIT binary patch literal 1563 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKV3x~tb`J1#c2)=|%1_J8No8QD zU~CP!pM5z%pKTcrzf+GR}4O9?JiK}KQU6w@P04YHl8Q~ZV% zQ(n$q`&^(mcb25z#hR{r@|Lv)J@e{@B!Rx#qQchktv&e=X{IS~=;%>n(D`^nj5%7>4X2ih}x zI5qXjq-Z>t=){vaEBVN(A3kD|N?uDYWtN0gZnC&$tucApTN(F?lkt2z;>5m2Z`$$g z!S8u@Rj>E$5p6xXeesRJw_6)u?%E_gOS--^ap~f`hvqw<&X&$y&CL<`uj}bXkGRxR z{!M|`OU<552%i3@l>5BVly|CSLJ>T>)_jQjp_Rks-zt7+`u-~?&fQxpc)Fob$8Pn! z;Iqfxs9y59^g3n7{;sc1$0UAEFm#TZyHCY;PM77UTLIkaY~@X_pFN7vzs^2y_nFv- zzjku0)hkl&J0$+qwMOy7KI46M@!Rt|5|`iJcRA(tV#BD{yDv}NwP<72l>Xc5-)#F< z2}^F;xNl=!7>D+jMafxSn@ju|m{oKArhjIg{9dqyyQX8EZfF1Mr0@y;+FbcFdE%z? zc&_)jskgzp|1I;j;A^qZGUJOaKWi1dl>U48wVrt>Tb$&9i^Y243ye#jaBirH(|Ij= zTi#~g_J)bpx0Bb}<|gjywhLQ*yF@Qqj&V2t>Z|k5uey5o`ICC?ip77}Rw{E{JG3y~ z2$*3w3p^r=85p>QL70(Y)*J~22FAS1kcg59UmvUF{9L`nl>DSry^7odplSvNn+hu+ zGdHy)QK2F?C$HG5!d3~a!V1U+3F|8h+i#(Mch>H3D2mX`VkM*2oZx=P7{9O-#x!EwNQn0$BtH5O<~|!|f?3Ey@Agl9H^SnvVRe4>ugB*Cr*|s<<>MD>b~6SmU=KJYrt2dK+vsDew$TT<2+4&I?}J5w z{omQijEVDI+SRi%2PRL?xK-R>xVmmD@5`vf?zYm1=C|9@ies7iF4l5eHiQ-L54C7! z{BuBQ$1S}z4d3|87yRRWRbcy{pSW&f{>J60Sqv~jRIa^sH0t3$g_Z@HVsxj!&(XV}x*{|?!mGCI@zJ*MaE zO4Bul4cw0_S^jla&$uEnTl%VL%hscMRXNOmGQtfw&PwPx=1TlrwY*THOPR~>bBx^T pgN5_F + try + _gaq.push ['_trackEvent', category, action, label] + catch error + +jQuery ($) -> + window.rabel.sortable = (selector, update_path, options) -> + options ||= {} + settings = + stop: (event, ui) -> + $.post(update_path, $(this).sortable('serialize', {key: 'position[]'}), -> + $(ui.item).parent().effect('highlight') + ) + $.extend(settings, options) + $(selector).sortable(settings) + + $(".highlight").mouseenter -> + $(this).css('background', '#f0f0f0') + .mouseleave -> + $(this).css('background', '') + $("textarea").elastic() + $("#Search input").keyup (ev) -> + if ev.which == 13 + query = $(this).val() + return if query.length == 0 + domain = $(this).data('domain') + window.open "#{window.rabel.search_engine_url}site:#{domain}%20#{query}" + + focus_comment_box = -> + $("#comment_content").focus() + + $(".fix_cell").find(".cell:last").addClass("inner").removeClass("cell") + $(".mention_button").click -> + mention = $(this).data('mention') + current_content = $("#comment_content").val() + new_content = '' + if current_content.length > 0 + new_content = current_content + "\n" + mention + ' ' + else + new_content = mention + ' ' + focus_comment_box().val(new_content) + $(".jump_to_comment").click -> + $.smoothScroll({speed: 700, scrollTarget: '.reply_content:last'}) + focus_comment_box() + $(".back_to_top").click -> + $.smoothScroll({speed: 700, scrollTarget: '#Top'}) + + $("a.preview").click -> + ref_obj = $("#" + $(this).data('ref')) + preview_content = ref_obj.val() + if preview_content.length == 0 + ref_obj.focus() + return + + type = $(this).data('type') + $.post("/topics/preview", {content: preview_content, type: type}, (data) -> + ref_obj.hide() + $("#preview").html(data).show() + $("#preview").css('border', '1px dotted #ccc') + $("#preview").css('background', 'lightyellow') + $("#preview").css('padding', '10px') + $("a.preview").hide() + $(".cancel_preview").show() + ) + $("a.cancel_preview").click -> + content_id = $(this).data('ref') + ref_obj = $("#" + content_id) + $("#preview").hide() + ref_obj.show() + ref_obj.focus() + $("a.preview").show() + $(this).hide() + + $(".track_event").click -> + window.rabel.trackEvent($(this).data('category'), $(this).data('action'), $(this).data('label')) + + $.datepicker.setDefaults($.datepicker.regional['zh-CN']) + $(".datepicker").datepicker({showButtonPanel: true}) + + $(".hoverable").mouseenter -> + $(this).find('.hover_action').fadeIn() + .mouseleave -> + $(this).find('.hover_action').fadeOut() + + if window.location.hash.length > 0 + hashbang = window.location.hash.split('/') + if hashbang[0] == '#!' and hashbang[1] == 'click' + $("##{hashbang[2]}").click() + diff --git a/app/assets/stylesheets/admin/i_base.css.scss.erb b/app/assets/stylesheets/admin/i_base.css.scss.erb new file mode 100644 index 0000000..5f85a0c --- /dev/null +++ b/app/assets/stylesheets/admin/i_base.css.scss.erb @@ -0,0 +1,43 @@ +#Wrapper { + background: url(<%= asset_path('admin/bg.png') %>) repeat; +} + +#Top { + background: #303033; +} + +table.admin { + td { text-align: center; } +} + +#AdminMenu{ + float: left; + width: 125px; +} + +#AdminContent { + margin: 0px 0px 0px 145px; + .column .box { + width: 95%; + } +} + +.current_item { + font-weight: bold; + color: black !important; + font-size: 14px; +} + +.published { + color: green; + font-weight: bold; +} + +.draft { + color: #999; + font-style: italic; +} + +.mll { height: 40em; } + +textarea.ml, textarea.sl { width: 580px; } diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..57aa4e6 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,8 @@ +/* + *= require desktop + *= require jquery.facebox + *= require jquery_ui + *= require shared + *= require rabel + */ + diff --git a/app/assets/stylesheets/desktop.css.scss.erb b/app/assets/stylesheets/desktop.css.scss.erb new file mode 100644 index 0000000..94a0323 --- /dev/null +++ b/app/assets/stylesheets/desktop.css.scss.erb @@ -0,0 +1,998 @@ +/* + V2EX CSS (Desktop) + + Author: Livid + Web: http://picky.olivida.com/ + + This is the desktop configuration of style for Project Babel. + + The best way to wipe IE6 from this planet is to forget it all since the beginning of your every new project. +*/ + +html { +} + +body { + padding: 0px; + margin: 0px; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; + background-color: #fff; +} + +h1 { + font-size: 24px; + line-height: 150%; + font-weight: 500; + margin: 0px 0px 10px 0px; + padding: 0px; +} + +h2 { + font-size: 18px; + line-height: 20px; + font-weight: bold; + padding: 10px 0px 10px 0px; + margin: 0px; +} + +h3 { + font-size: 15px; + line-height: 18px; + font-weight: bold; + padding: 6px 0px 6px 0px; + margin: 0px; +} + +h4 { + font-size: 20px; + line-height: 20px; + font-weight: 500; + padding: 0px 0px 0px 0px; + margin: 0px; +} + +form { + display: inline; + padding: none; + margin: none; +} + +code { + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Luxi Mono", "Courier New", Monaco, "Hiragino Sans GB", STHeiti !important; +} + +ul, ol { + margin: 0px; + padding: 0px; +} + +ul li { + list-style: square; + padding: 0px; + margin: 0px 0px 0px 1.2em; +} + +ol li { + padding: 0px; + margin: 0px 0px 0px 1.5em; +} + +table { width: 100%; } + +a:link, a:visited, a:active { + color: #778087; + text-decoration: none; +} + +a:hover { + color: #4d5256; + text-decoration: underline; +} + +a.top:link, a.top:visited, a.top:active { + color: #778087; + text-decoration: none; + text-shadow: 0px 1px 0px #fff; + font-weight: 500; +} + +a.top:hover { + color: #4d5256; + text-decoration: none; + text-shadow: 0px 1px 0px #fff; + font-weight: 500; +} + +a.white:link, a.white:visited, a.white:active { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; +} + +a.white:hover { + color: rgba(255, 255, 255, 1); + text-decoration: none; +} + +a.black:link, a.black:visited, a.black:active { + color: rgba(0, 0, 0, 1); + text-decoration: none; +} + +a.black:hover { + color: rgba(0, 0, 0, 1); + text-decoration: underline; + text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.2); +} + +a.dark:link, a.dark:visited, a.dark:active { + color: gray; + text-decoration: none; +} + +a.dark:hover { + color: #385f8a; + text-decoration: none; +} + +/* IDs */ + +#Top { + height: 40px; + background-color: #f0f0f0; + background-image: url(<%= asset_path('bg_top_light.png') %>); +} + +#TopMain { + width: 960px; + height: 40px; + text-align: center; + margin: 0px auto 0px auto; +} + +#Search { + text-align: left; + padding: 6px 0px 0px 0px; +} + +#q { + border: none; + width: 222px; + height: 26px; + margin: 1px 0px 1px 30px; + background-color: transparent; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; + font-size: 12px; + line-height: 20px; + outline: none; +} + +#Navigation { + float: right; + text-align: right; + font-size: 14px; + color: #fff; + margin-top: 13px; + line-height: 14px; +} + +#Navigation ul { + margin: 0px; + padding: 0px; + list-style: none; +} + +#Navigation ul li { + list-style: none; + float: left; + margin-left: 10px; +} + +#Wrapper { + background-color: #E0E0E0; +} + +#Content { + width: 960px; + margin: 0px auto 0px auto; +} + +#Sidebar { + float: left; + width: 0px; + background-color: red; +} + +#Rightbar { + float: right; + width: 270px; +} + +#Main { + margin: 0px 290px 0px 0px; +} + +#Bottom { + background-color: #fff; + border-top: 1px solid #ccc; +} + +#BottomMain { + margin: 0px auto 0px auto; + width: 920px; + padding: 20px 0px 20px 0px; + font-size: 12px; + color: #e2e2e2; +} + +/* CLASSes */ + +.box { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); + -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); + + background-color: #fff; + border: 2px solid #fff; +} + +.glass { + border: 2px dashed rgba(0, 0, 0, 0.1); + border-radius: 3px; + color: gray; +} + +.header { + padding: 10px; + font-size: 14px; + line-height: 120%; + text-align: left; + border-bottom: 1px solid #E2E2E2; +} + +.box .inner, .glass .inner { + padding: 10px; + font-size: 12px; + line-height: 16px; +} + +.box .highlighted { + border-left: 3px solid #3c3; + background-color: #f5f5f5; +} + +.box .yellow { + padding: 10px; + font-size: 12px; + line-height: 16px; + background-color: #ffffe2; + border-top: 1px solid #ffffcc; + -moz-border-radius: 0px 0px 6px 6px; +} + +.box .cell { + padding: 10px; + font-size: 12px; + line-height: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.box .bar { + font-size: 14px; + line-height: 14px; + color: #667; + background-color: #f0f0f0; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + padding: 5px; + display: inline-block; +} + +td.avatar { + width: 48px; + text-align: center; + vertical-align: top; +} + +td.avatar_mini { + width: 24px; + text-align: center; + vertical-align: top; +} + +.created { + font-size: 11px; + color: #ccc; + display: block; + -webkit-text-size-adjust: none; +} + +.avatar_normal { + max-width: 48px; + max-height: 48px; +} + +.note { + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Luxi Mono", "Courier New", "Helvetica Neue", "Tahoma", "Verdana", "Hiragino Sans GB", STHeiti !important; + font-size: 14px; + line-height: 180%; +} + +.page { + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; + font-size: 14px; + line-height: 180%; + padding: 0px 20px 20px 20px; +} + +.status { + font-size: 14px; + line-height: 20px; +} + +.bigger { + font-size: 16px; +} + +.footer { + font-size: 12px; + line-height: 18px; + color: #aaa; +} + +.content { + font-size: 14px; + line-height: 180%; + color: #000; + overflow:hidden; + word-break: break-word; +} + +.f12 { font-size: 12px; } + +.green { + color: #393; +} + +.red { color: #ff6666; } + +.sky { + color: #69859d; +} + +.white { + color: rgba(255, 255, 255, 0.75); +} + +.orange { + color: #ff9933; +} + +.fade { + color: #999; +} + +.gray { + color: #999; +} + +.snow { + color: rgba(0, 0, 0, 0.15); +} + +.chevron { + color: #666; + font-family: "Lucida Grande"; +} + +.sep3 { + height: 3px; +} + +.sep5 { + height: 5px; +} + +.sep10 { + height: 10px; +} + +.sep20 { + height: 20px; +} + +.c { + clear: both; +} + +.fr { + float: right; + text-align: right; +} + +.fl { + float: left; +} + +.item_node { + font-size: 12px; + line-height: 12px; + padding: 4px 10px 4px 10px; + margin: 0px 5px 5px 0px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + display: inline-block; + background: white; + background: -moz-linear-gradient(top, white 0%, #F3F3F3 50%, #EDEDED 51%, white 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,white), color-stop(50%,#F3F3F3), color-stop(51%,#EDEDED), color-stop(100%,white)); + background: -webkit-linear-gradient(top, white 0%,#F3F3F3 50%,#EDEDED 51%,white 100%); + background: -o-linear-gradient(top, white 0%,#F3F3F3 50%,#EDEDED 51%,white 100%); + background: -ms-linear-gradient(top, white 0%,#F3F3F3 50%,#EDEDED 51%,white 100%); + background: linear-gradient(top, white 0%,#F3F3F3 50%,#EDEDED 51%,white 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='white', endColorstr='white',GradientType=0 ); + border: 1px solid #E5E5E5; +} + +.item_node:hover { + text-decoration: none; + color: #333; + border: 1px solid #ccc; +} + +table.topics { + width: 100%; + tr td:first-child { + text-align: center; + } + tr td:last-child { + text-align: center; + } + td, th { + padding: 5px; + } +} + +.topics td, .topics th { + line-height: 24px; + font-size: 14px; +} + +.w50 { + width: 50px; +} + +.w60 { + width: 60px; +} + +.w100 { + width: 100px; +} + +.w120 { + width: 120px; +} + +.w200 { + width: 200px; +} + +.auto { + width: auto; +} + +.topics th { + color: #ccc; + font-weight: bold; +} + +.topics .odd { + background-color: transparent; +} + +.topics .even { + background-color: #f9f9f9; +} + +.topics .lend { + -moz-border-radius: 3px 0px 0px 3px; +} + +.topics .rend { + -moz-border-radius: 0px 3px 3px 0px; +} + +.super.button { + background-image: url(<%= asset_path 'bg_blended.png' %>); + padding: 4px 15px 4px 15px; + border: 1px solid rgba(80,80,90, 0.2); + border-bottom-color: rgba(80,80,90, 0.35); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + font-size: 12px; + line-height: 12px; + outline: none; +} + +.normal.button { + background-color: #f0f4f7; + color: #333; + text-shadow: 0px 1px 0px #fff; + text-decoration: none; + font-weight: bold; + -moz-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.25); +} + +.normal.button:hover { + background-color: #fff; + color: #333; + text-shadow: 0px 1px 0px #fff; + text-decoration: none; + font-weight: bold; + cursor: pointer; + -moz-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.2); + -webkit-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.2); +} + +.normal.button:active { + background-color: #e2e2e2; + color: #333; + text-shadow: 0px 1px 0px #fff; + text-decoration: none; + font-weight: bold; + cursor: pointer; + -moz-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.2); + -webkit-box-shadow: 0px 1px 0px rgba(66, 66, 77, 0.2); +} + +.special.button { background-color: #ffcc00; color: #532b17; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.6); text-decoration: none; font-weight: 600; -moz-box-shadow: 0px 1px 2px rgba(233, 175, 0, 0.6); border: 1px solid rgba(200, 150, 0, 0.8); } +.special.button:hover { background-color: #ffdf00; color: #402112; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.7); text-decoration: none; text-decoration: none; font-weight: 600; cursor: pointer; -moz-box-shadow: 0px 1px 2px rgba(233, 175, 0, 0.5); border: 1px solid rgba(200, 150, 0, 1); } +.special.button:active { background-color: #ffbb00; color: #402112; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.7); text-decoration: none; text-decoration: none; font-weight: 600; cursor: pointer; -moz-box-shadow: 0px 1px 2px rgba(233, 175, 0, 0.5); border: 1px solid rgba(200, 150, 0, 1); } + +.inverse.button { background-color: #ccc; color: #999; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.6); text-decoration: none; font-weight: 600; -moz-box-shadow: 0px 1px 2px rgba(200, 200, 200, 0.8); border: 1px solid rgba(150, 150, 150, 0.8); } +.inverse.button:hover { background-color: #999; color: #fff; text-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5); text-decoration: none; text-decoration: none; font-weight: 600; cursor: pointer; -moz-box-shadow: 0px 1px 2px rgba(200, 200, 200, 1); border: 1px solid rgba(150, 150, 150, 0.6); } +.inverse.button:active { background-color: #888; color: #fff; text-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5); text-decoration: none; text-decoration: none; font-weight: 600; cursor: pointer; -moz-box-shadow: 0px 1px 2px rgba(200, 200, 200, 1); border: 1px solid rgba(150, 150, 150, 0.6); } + +.danger.button { background-color: #900; color: #fff; text-decoration: none; font-weight: 600; border: 1px solid #800; } +.danger.button:hover { background-color: #C00; } +.danger.button:active { background-color: #D00; } +/* FORM */ + +.sl { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + width: 320px; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.sl:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.sls { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + width: 120px; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.sls:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.search { + -moz-border-radius: 3px 0px 0px 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border-top: 1px solid #fff; + border-bottom: 1px solid #fff; + border-left: 1px solid #fff; + width: 240px; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.search:focus { + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.sll { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + width: 628px; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.sll:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.mll { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + display: block; + width: 628px; + height: 8em; + overflow-y: auto; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.mll:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.mlt { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + display: block; + width: 242px; + height: 50px; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.mlt:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.mle { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + display: block; + width: 618px; + height: 100px; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.mle:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +.short { + height: 52px; +} + +.tall { + height: 320px; +} + +/* VARIOUS COUNT */ + +a.count { + line-height: 12px; + font-weight: bold; + color: white; + background-color: #aab0c6; + display: block; + padding: 2px 10px 2px 10px; + border-radius: 12px; + margin: 4px 12px 0px 0px; + text-decoration: none; +} + +a.count:hover { + background-color: #969cb1; +} + +.time td.city { + color: #667; + font-size: 16px; +} + +.time td.now { + color: #000; + font-size: 16px; + font-weight: bold; +} + +table.grid { + border-collapse: collapse; + border-left: none; + border-right: none; +} + +table.grid td { + border: 1px solid #e2e2e2; + border-left: 1px solid #e2e2e2; + border-right: none; + height: 120px; +} + +table.grid td.left { + border-left: none; +} + +.gist { font-size: 11px; font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Luxi Mono", "Courier New", Monaco, "Hiragino Sans GB", STHeiti !important; } + +.gist-file { + -moz-border-radius: 4px; -webkit-border-radius: 4px; + border: 2px solid #999; + -moz-box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1); +} + +.gist-file .gist-data { + -moz-border-radius: 4px 4px 0px 0px; -webkit-border-radius: 4px; +} + +.cell .gist { max-width: 576px; } + +.place_title { + font-family: "DINPro"; + font-size: 32px; + line-height: 32px; + padding: 0px; + margin: 0px; +} + +.place_visitors { + font-family: "DINPro"; + font-size: 14px; + line-height: 14px; + color: #999; +} + +.place_say { + font-family: "DINPro"; + font-size: 14px; + line-height: 14px; + color: #999; +} + +a.tiny_label:link, a.tiny_label:visited, a.tiny_label:active { + font-size: 10px; + line-height: 10px; + color: #fff; + background-color: #dde; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 2px 5px 2px 5px; +} + +a.tiny_label:hover { + text-decoration: none; + background-color: #99a; +} + +a.node:link, a.node:visited, a.node:active { + background-color: #f5f5f5; + font-size: 10px; + line-height: 10px; + display: inline-block; + padding: 4px 4px 3px 4px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + text-decoration: none; + color: #999; +} + +a.node:hover { + text-decoration: none; + background-color: #e2e2e2; + color: #777; +} + +a.op:link, a.op:visited, a.op:active { + background-color: #f0f0f0; + font-size: 10px; + line-height: 10px; + display: inline-block; + padding: 3px 4px 3px 4px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + text-decoration: none; + border: 1px solid #ddd; + color: #666; +} + +.hover_action a { color: rgba(0, 0, 0, 0.3); } + +a.op:hover { + text-decoration: none; + background-color: #e0e0e0; + border: 1px solid #c0c0c0; + color: #333; +} + +a.op_danger:hover { + color: #F97171; +} + + + +a.opo:link, a.opo:visited, a.opo:active { + background-color: #333; + font-size: 10px; + line-height: 10px; + display: inline-block; + padding: 3px 4px 3px 4px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + text-decoration: none; + border: 1px solid #000; + color: #eee; +} + +a.opo:hover { + text-decoration: none; + background-color: #666; + border: 1px solid #333; + color: #fff; +} + +.clickable { + cursor: pointer; +} + +.reply_content .imgly { + max-width: 570px; +} + +.topic_content .imgly { + max-width: 635px; +} + +.howmany { + display: inline-block; + font-size: 14px; + line-height: 14px; + padding: 3px 5px 3px 5px; + background-color: #f5f5ff; + border: 1px solid #f0f0ff; + color: #99a; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +/* PAGE */ + +.page_current { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #f0f0f0; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; +} + +.page_normal:link, .page_normal:visited, .page_normal:active { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; +} + +.page_normal:hover { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #ff9933; + color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; +} + +.afterdark { + padding: 10px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + background-color: #333; + color: #cc9; + font-family: "Panic Sans"; + font-size: 16px; + line-height: 180%; + width: 616px; + height: 500px; +} + +a.slot:link, a.slot:visited, a.slot:active { + display: block; + margin: 10px 10px 10px 10px; + border: 2px solid #f0f0f0; + display: inline-block; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + font-size: 12px; + background-color: none; + color: transparent; +} + +a.slot:hover { + display: block; + margin: 10px 10px 10px 10px; + border: 2px solid #99c; + display: inline-block; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + font-size: 12px; + background-color: none; + color: transparent; +} + +.payload { + display: inline-block; + background-color: #f5f5f5; + padding: 5px 10px 5px 10px; + font-size: 14px; + line-height: 120%; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +a.fade:link, a.fade:visited, a.fade:active { + color: #999; + text-decoration: none; +} + +a.fade:hover { + color: #666; + text-decoration: none; +} + +small { + -webkit-text-size-adjust: none; +} + diff --git a/app/assets/stylesheets/i_mobile.css.erb b/app/assets/stylesheets/i_mobile.css.erb new file mode 100644 index 0000000..323af51 --- /dev/null +++ b/app/assets/stylesheets/i_mobile.css.erb @@ -0,0 +1,501 @@ +/* + *= require shared + * + */ + +@charset "utf-8"; + +/* + V2EX CSS (Mobile) + + Author: Livid + Web: http://picky.olivida.com/ + + This is the mobile configuration of style for Project Babel. + + The best way to wipe IE6 from this planet is to forget it all since the beginning of your every new project. +*/ + +html { + height: 100%; +} + +body { + padding: 0px; + margin: 0px; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; + background-color: #fff; + -webkit-text-size-adjust: none; +} + +a:link { + color: #385f8a; + text-decoration: none; + font-weight: bold; +} + +a:active { + color: #385f8a; + text-decoration: none; +} + +a:visited { + color: #7ca0c9; + text-decoration: none; +} + +a:hover { + color: #385f8a; + text-decoration: underline; +} + +a.white:link, a.white:visited, a.white:active { + color: rgba(255, 255, 255, 1); + text-decoration: none; +} + +a.white:hover { + color: rgba(255, 255, 255, 1); + text-decoration: underline; +} + +a.black:link, a.black:visited, a.black:active { + color: rgba(0, 0, 0, 1); + text-decoration: none; +} + +a.icon:link, a.icon:hover, a.icon:active, a.icon:visited { + text-decoration: none; +} + +a.black:hover { + color: rgba(0, 0, 0, 1); + text-decoration: underline; + text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.2); +} + +form { + display: inline; + padding: none; + margin: none; +} + +code { + font-family: "Menlo", "Panic Sans", "Luxi Mono", "Courier New", Monaco; +} + +ul { + margin: 0px; + padding: 0px; +} + +ul li { + list-style: square; + padding: 0px; + margin: 0px 0px 0px 1.2em; +} + +h1 { + color: #333; + font-size: 18px; + line-height: 18px; + padding: 0px; + margin: 0px; + font-weight: bold; +} + +/* IDs */ + +#Top { + padding: 10px; + background: #303033; + background: -moz-linear-gradient(center top , #444, #111) repeat scroll 0 0 transparent; + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#111)); + height: 24px; +} + +#Logo { + width: 70px; + height: 20px; + margin-top: 2px; + margin-left: 0px; + cursor: pointer; + font-size: 18px; +} + +#Logo:link, #Logo:hover, #Logo:visited { + color: white; + text-decoration: none; +} + +#Member { + font-weight: bold; + float: right; + font-size: 14px; + color: rgba(255, 255, 255, 0.25); +} + +#GearIcon { + display: inline-block; + width: 24px; + height: 24px; + background-image: url("<%= asset_path 'mobile/gear.png' %>"); +} + +#EjectIcon { + display: inline-block; + width: 24px; + height: 24px; + background-image: url("<%= asset_path 'mobile/eject.png' %>"); +} + +#Main { + padding: 0px; + font-size: 14px; + line-height: 18px; +} + +#SponsoredLeft { + float: left; + width: 44px; + height: 44px; +} + +#SponsoredRight { + float: right; + width: 44px; + height: 44px; +} + +#SponsoredMain { + margin: 0px 44px 0px 44px; + height: 44px; +} + +/* CLASSes */ + +.section { + background-color: #bbb; + background-image: url("<%= asset_path 'mobile//bg_section.png' %>"); + font-size: 12px; + line-height: 12px; + height: 12px; + color: #333; + font-weight: bold; + padding: 3px 5px 5px 5px; + text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); +} + +.cell { + padding: 5px 5px 5px 5px; + font-size: 12px; + line-height: 14px; + border-bottom: 1px solid #ccc; +} + +.cell_bottom { + padding: 4px 5px 5px 5px; + font-size: 11px; + line-height: 11px; + color: #999; +} + +td.avatar { + width: 48px; + text-align: center; + vertical-align: top; +} + +td.avatar_mini { + width: 24px; + text-align: center; + vertical-align: top; +} + +.created { + font-size: 10px; + font-weight: bold; + color: #ccc; + display: block; +} + +.bigger { + font-size: 16px; +} + +.caption { + color: #999; + font-size: 13px; + font-weight: bold; +} + +.caption_alternative { + color: #ccc; + font-size: 12px; + font-weight: bold; +} + +.sep10 { + height: 10px; +} + +.sep5 { + height: 5px; +} + +.sep0 { + height: 0px; +} + +.sep { + height: 1px; + background-color: #e2e2e2; + margin-top: 10px; + margin-bottom: 10px; +} + +.dot { + border-top: 1px dotted #e2e2e2; + margin-top: 10px; + margin-bottom: 10px; +} + +.signin { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 12px; + font-weight: bold; + border: 1px solid #ccc; + width: 150px; +} + +.signin:focus { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 12px; + font-weight: bold; + border: 1px solid #889fb9; + width: 150px; + -moz-box-shadow: 0px 0px 5px #889fb9; + -webkit-box-shadow: 0px 0px 5px #889fb9; + outline: none; +} + +.sl, .sls { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: bold; + border: 1px solid #ccc; + width: 150px; +} + +.sl:focus, .sls:focus { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: bold; + border: 1px solid #889fb9; + -moz-box-shadow: 0px 0px 5px #889fb9; + -webkit-box-shadow: 0px 0px 5px #889fb9; + outline: none; +} + +.sll { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: bold; + border: 1px solid #ccc; + width: 300px; +} + +.sll:focus { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: bold; + border: 1px solid #889fb9; + -moz-box-shadow: 0px 0px 5px #889fb9; + -webkit-box-shadow: 0px 0px 5px #889fb9; + outline: none; +} + +.mll { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: normal; + border: 1px solid #ccc; + display: block; + width: 300px; + height: 160px; + font-family: "Menlo", "Panic Sans", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.mll:focus { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + font-weight: normal; + border: 1px solid #889fb9; + -moz-box-shadow: 0px 0px 5px #889fb9; + -webkit-box-shadow: 0px 0px 5px #889fb9; + outline: none; +} + +.fr { + float: right; +} + +.topic_stats { + font-size: 10px; + font-weight: bold; + color: #ccc; +} + +.fade { + color: #999; +} + +.snow { + color: rgba(0, 0, 0, 0.15); +} + +.black { + color: #000; +} + +a.topic:link { + color: #555; + text-decoration: none; + font-weight: bold; + font-size: 16px; +} + +a.topic:active { + color: #555; + text-decoration: none; + font-size: 16px; +} + +a.topic:visited { + color: #999; + text-decoration: none; + font-size: 16px; +} + +a.topic:hover { + color: #555; + text-decoration: underline; + font-size: 16px; +} + +.reply { + color: #333; + border-top: 1px dotted #ccc; + padding: 5px 0px 0px 0px; +} + +.ago { + font-size: 10px; + font-weight: bold; + color: #ccc; + line-height: 10px; +} + +.imgly { + padding: 2px; + border: 1px solid #e2e2e2; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.33); + -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.33); +} + +input.btn { + font-size: 13px; + font-weight: bold; + color: #333; +} + +/* High Resolution (iPhone 4) */ + +@media only screen and (-webkit-min-device-pixel-ratio: 2) { + #GearIcon { + background-image: url(<%= asset_path('mobile/gear48.png') %>); + background-size: 24px 24px; + } + + #EjectIcon { + background-image: url(<%= asset_path('mobile/eject48.png') %>); + background-size: 24px 24px; + } +} + +/* Added by Devin Zhang */ +.page_current { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #f0f0f0; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; +} + +.page_normal:link, .page_normal:visited, .page_normal:active { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; +} + +.page_normal:hover { + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #ff9933; + color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; +} + +.hide { display: none; } + +.pagination span { + margin: 0; + padding: 0; + font-size: 12px; + line-height: 12px; +} + +.pagination span a:link { + margin: 0; + font-size: 12px; + line-height: 12px; +} + +img.external { + max-width: 260px; +} diff --git a/app/assets/stylesheets/rabel.css.scss.erb b/app/assets/stylesheets/rabel.css.scss.erb new file mode 100644 index 0000000..079586b --- /dev/null +++ b/app/assets/stylesheets/rabel.css.scss.erb @@ -0,0 +1,223 @@ +$current-border-color: #4D90F0; +$current-border-style: 2px solid $current-border-color; + +img { + border: none; +} + +a.logo { + display: block; + float: left; + margin-right: 20px; +} + +a.custom_logo { + margin-top: 0; +} + +a.text_logo { + margin-top: 5px; + height: 23px; + font-size: 24px; + color: #5d6469; + font-weight: bold; +} + +a.logo:hover, a.logo:visited { + text-decoration: none; + color: #5d6469; +} + +div.search_input { + width: 276px; + height: 28px; + background-image: url(<%= asset_path('qbar.png') %>); + background-repeat: no-repeat; + display: inline-block; +} + +.hide { display: none; } + +div.all_topics { + padding: 3px 10px 5px 10px; + display: inline-block; + /*border-bottom: 5px solid #3c3;*/ + margin: 0px 2px 0px 2px; +} + +.hero_unit { + background-color: whiteSmoke; + margin-bottom: 10px; + padding: 30px 50px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + + h1 { + margin-bottom: 0; + font-size: 40px; + line-height: 1; + letter-spacing: -1px; + font-weight: bold; + color: #404040; + } + + p { + font-size: 18px; + font-weight: 200; + line-height: 18px; + margin-bottom: 9px; + } +} + + +td.ruler { + padding: 10px 5px 10px 5px; + height: 2px; +} + +div.ruler { + background-color: #f0f0f0; + height: 2px; +} + +table.form { + border-collapse: collsapse; + border: none; + td { padding: 5px; } + td.left { + text-align: right; + width: 120px; + } + + td.right { + text-align: left; + width: 200px; + } +} + +img.dot_icon { + position: relative; + top: -1px; +} + +span.validation_error { + color: #ee3838; + font-size: 12px; + display: block; +} + +.smaller { + font-size: 80%; + color: #999; + margin-left: 2px; +} + +form label { + font-size: 14px; +} + +.advertisement { + padding: 20px; + float: left; + border: 1px solid #E2E2E2; + margin-right: 20px; + margin-bottom: 20px; + background: #eee; + + .fr { + margin-top: -1px; + } +} + +img.external { max-width: 570px; } + +.guake { + position: fixed; + top: 0; + width: 100%; + height: 100%; + display: none; + background-color: #222; + color: #909090; + font-family: monospace; + padding-top: 15px; + text-align: center; + opacity: 0.95; + + .key { text-align: right; } + .desc { + color: #DDD; + + code { + border: 1px solid #CCC; + padding: 3px 5px; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0 5px; + } + } + + a:link, a:active, a:visited { color: #ddd; text-decoration: underline; } + + table { margin: 0 auto; } +} + +#shortcuts { + z-index: 9999; +} + +#markdown { + z-index: 999; +} + +.yellow { color: yellow; } + +.current_pointer { + background: #eee; + border-left: $current-border-style; +} + +.ui-sortable tr, .ui-sortable .node, .sort_item { + cursor: move; + &:hover { + background: #eee; + border-left: $current-border-style; + } +} + +.sort_item { + padding: 5px; + border-bottom: 1px dotted #DDD; +} + +.sort_actions { + text-align: center; + margin-top: 10px; +} + + +td.desc { + text-align: left; +} + +.hover_action { display: none; } + +a.admin_op:link, a.admin_op:visited, a.admin_op:active { + padding: 5px 8px; + color: #333; +} + +#Banner { width: 960px; margin: 0 auto; } +strong.danger { color: #900; } +a.action_label { + border: 1px solid #CCC; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + padding: 5px 8px 2px; + color: #333; + border-bottom: none; +} + +label.small { font-size: 12px; } diff --git a/app/assets/stylesheets/shared.css.scss b/app/assets/stylesheets/shared.css.scss new file mode 100644 index 0000000..02790b1 --- /dev/null +++ b/app/assets/stylesheets/shared.css.scss @@ -0,0 +1,215 @@ +pre { + color: #444; + margin: 0; +} + +.code { + background: #F8F8F8; + border: 1px solid #CCC; + font-size: 13px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} + +p > code, li > code { + padding: 1px 3px; + margin: 0 3px; + font-size: 12px; + color: #52595d; +} + +.CodeRay { line-height: 5px; } +.content p { margin: 0 0 1em 0; } +.page p { margin: 0 0 3px 0; } + +ol { margin-left: 10px; } +ul li { margin-left: 1em; } + +.ml { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding: 3px; + font-size: 14px; + border: 1px solid #ccc; + display: block; + width: 320px; + height: 160px; + font-family: "Panic Sans", "Menlo", "DejaVu Sans Mono", "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; +} + +.ml:focus { + border: 1px solid rgba(128, 128, 160, 0.6); + -moz-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + -webkit-box-shadow: 0px 0px 5px rgba(128, 128, 160, 0.5); + outline: none; +} + +span.nickname { + position: relative; + top: -7px; + color: #888; + font-size: 11px; +} + +td.with_separator { + border-right: 1px solid rgba(100, 100, 100, 0.4); +} + +td.with_background { + background-color: #f9f9f9; + border-left: 1px solid #f0f0f0; + font-size: 12px; + padding: 10px; +} + +.pagination { + span { + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 0px 5px; + + &.current { + color: #333; + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 2px 5px 2px 5px; + background-color: #F0F0F0; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + } + + & a:link, & a:visited, & a:active { + display: inline-block; + padding: 2px 5px 2px 5px; + background-color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; + } + + & a:hover { + display: inline-block; + padding: 2px 5px 2px 5px; + background-color: #F93; + color: #fff; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + margin: 0px 2px 0px 2px; + text-decoration: none; + } + } + + .gap { color: #666; } +} + +#BottomMain { + color: #999; + span.divider, small { color: #e2e2e2; } +} + +table.reply { + width: 100%; + max-width: 100%; + border-spacing: 0; + margin-bottom: 18px; + border: 1px solid #DDD; + border-left: 0; + border-collapse: separate; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + + td, th { + padding: 8px; + line-height: 18px; + text-align: left; + vertical-align: top; + border-top: 1px solid #DDD; + border-left: 1px solid #DDD; + } + + & thead:first-child tr:first-child th { + border-top: 0; + } +} + +.hrule { border-top: 1px solid #E2E2E2; height: 5px; } + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #EEE; + width: 60%; + + ul { + color: #999; + + li { + list-style-type: none; + } + } +} + +h1 small { color: #999; } +.help-inline { margin-left: 5px; } +.center { text-align: center; } +.alert-message { color: #B94A48; } +.notice-message { color: #06D; } +.topics_heading { color: #aaa; text-align: center; } +.quantity { text-align: right; } + + +table.data td.h { + text-align: left; + font-size: 12px; + font-weight: bold; + border-right: 1px solid #CCC; + border-bottom: 2px solid #CCC; + text-shadow: 0px 1px 0px white; + background: whiteSmoke; + background: -moz-linear-gradient(top, whiteSmoke 0%, #E2E2E2 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,whiteSmoke), color-stop(100%,#E2E2E2)); + background: -webkit-linear-gradient(top, whiteSmoke 0%,#E2E2E2 100%); + background: -o-linear-gradient(top, whiteSmoke 0%,#E2E2E2 100%); + background: -ms-linear-gradient(top, whiteSmoke 0%,#E2E2E2 100%); + background: linear-gradient(top, whiteSmoke 0%,#E2E2E2 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='whiteSmoke', endColorstr='#E2E2E2',GradientType=0 ); +} + +table.data td.d { + text-align: left; + font-size: 12px; + font-weight: normal; + border-right: 1px solid #CCC; + border-bottom: 1px solid #CCC; +} + +table.data td.last { + border-right: none; +} + +img.mini_avatar { + max-width: 24px; + max-height: 24px; +} + +img.medium_avatar { + max-width: 48px; + max-height: 48px; +} + +img.large_avatar { + max-width: 72px; + max-height: 72px; +} + diff --git a/app/controllers/admin/advertisements_controller.rb b/app/controllers/admin/advertisements_controller.rb new file mode 100644 index 0000000..75bcc20 --- /dev/null +++ b/app/controllers/admin/advertisements_controller.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 +class Admin::AdvertisementsController < Admin::BaseController + before_filter :find_ad, :only => [:edit, :update, :destroy] + + def index + @ads = Advertisement.order('start_date DESC').page(params[:page]).per(4) + @title = '广告位' + end + + def new + @ad = Advertisement.new + @title = '添加新广告' + end + + def create + @ad = Advertisement.new(params[:advertisement]) + if @ad.save + redirect_to admin_advertisements_path + else + flash[:error] = '添加新广告失败' + render :new + end + end + + def edit + @title = '修改广告' + end + + def update + if @ad.update_attributes(params[:advertisement]) + redirect_to admin_advertisements_path + else + flash[:error] = '修改广告失败' + render :edit + end + end + + def destroy + if @ad.destroy + flash[:success] = '删除成功' + else + flash[:success] = '删除失败' + end + redirect_to admin_advertisements_path + end + + private + def find_ad + @ad = Advertisement.find(params[:id]) + end +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 0000000..763ecf1 --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,26 @@ +# encoding: utf-8 +class Admin::BaseController < ApplicationController + include Admin::BaseHelper + before_filter :authenticate_user! + before_filter do |c| + raise CanCan::AccessDenied unless current_user.can_manage_site? + end + + before_filter do |c| + add_title_item '管理后台' + end + + layout 'admin' + + def method_missing(method) + if method =~ /^find_parent_(.*)$/ + model = $1.classify.constantize + instance_variable_set("@#{$1}".to_sym, model.find(params["#{$1}_id".to_sym])) + elsif method =~ /^find_(.*)$/ + model = $1.classify.constantize + instance_variable_set("@#{$1}".to_sym, model.find(params[:id])) + else + super + end + end +end diff --git a/app/controllers/admin/cloud_files_controller.rb b/app/controllers/admin/cloud_files_controller.rb new file mode 100644 index 0000000..920d8b7 --- /dev/null +++ b/app/controllers/admin/cloud_files_controller.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 +class Admin::CloudFilesController < Admin::BaseController + def index + @title = '云硬盘' + @files = CloudFile.order('id DESC').page(params[:page]) + end + + def new + @file = CloudFile.new + @title = '上传新文件' + end + + def create + @file = CloudFile.new(params[:cloud_file]) + + if @file.save + redirect_to admin_cloud_files_path + else + @title = '上传新文件' + render :new + end + end + + def destroy + @file = CloudFile.find(params[:id]) + + if @file.destroy + flash[:success] = '删除成功' + else + flash[:error] = '删除失败' + end + redirect_to admin_cloud_files_path + end +end diff --git a/app/controllers/admin/nodes_controller.rb b/app/controllers/admin/nodes_controller.rb new file mode 100644 index 0000000..7e76789 --- /dev/null +++ b/app/controllers/admin/nodes_controller.rb @@ -0,0 +1,74 @@ +class Admin::NodesController < Admin::BaseController + before_filter :find_parent_plane, :except => [:sort, :destroy, :move, :move_to] + before_filter :find_node, :only => [:move, :move_to, :destroy] + + def new + @node = @plane.nodes.new + respond_to do |format| + format.js { render :show_form } + end + end + + def create + @node = @plane.nodes.build(params[:node]) + respond_to do |format| + if @node.save + format.js + else + format.js { render :show_form } + end + end + end + + def edit + @node = @plane.nodes.find(params[:id]) + respond_to do |format| + format.js { render :show_form } + end + end + + def update + @node = @plane.nodes.find(params[:id]) + respond_to do |format| + if @node.update_attributes(params[:node]) + format.js + else + format.js { render :show_form } + end + end + end + + def sort + params[:position].each_with_index do |id, pos| + Node.update(id, :position => pos) + end + + respond_to do |format| + format.js { head :ok } + end + end + + def move + respond_to do |f| + f.js + end + end + + def move_to + respond_to do |f| + f.js { + render :text => :error, :status => :unprocessable_entity unless @node.update_attributes(params[:node]) + } + end + end + + def destroy + respond_to do |format| + if @node.can_delete? and @node.destroy + format.js + else + format.js { render :text => :error, :status => :unprocessable_entity } + end + end + end +end diff --git a/app/controllers/admin/notifications_controller.rb b/app/controllers/admin/notifications_controller.rb new file mode 100644 index 0000000..43688a2 --- /dev/null +++ b/app/controllers/admin/notifications_controller.rb @@ -0,0 +1,8 @@ +class Admin::NotificationsController < Admin::BaseController + # only delete read notifications + def clear + Notification.where(:unread => false).delete_all + + redirect_to admin_root_path + end +end diff --git a/app/controllers/admin/pages_controller.rb b/app/controllers/admin/pages_controller.rb new file mode 100644 index 0000000..3db9e4f --- /dev/null +++ b/app/controllers/admin/pages_controller.rb @@ -0,0 +1,59 @@ +# encoding: utf-8 +class Admin::PagesController < Admin::BaseController + before_filter :find_page, :only => [:edit, :update, :destroy] + + def index + @pages = Page.default_order + @title = '页面' + end + + def new + @page = Page.new + @title = '创建新页面' + render :action + end + + def create + @page = Page.new(params[:page]) + if @page.save + redirect_to admin_pages_path + else + @title = '创建新页面' + render :action + end + end + + def edit + @title = '修改页面' + render :action + end + + def update + if @page.update_attributes(params[:page]) + redirect_to admin_pages_path + else + @title = '编辑页面' + render :action + end + end + + def destroy + if @page.destroy + redirect_to admin_pages_path + else + flash[:error] = '删除页面出错' + redirect_to admin_root_path + end + end + + def sort + params[:position].each_with_index do |id, pos| + Page.update(id, :position => pos) + end + + respond_to do |format| + format.js { head :ok } + end + end + +end diff --git a/app/controllers/admin/planes_controller.rb b/app/controllers/admin/planes_controller.rb new file mode 100644 index 0000000..8827d32 --- /dev/null +++ b/app/controllers/admin/planes_controller.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 +class Admin::PlanesController < Admin::BaseController + before_filter :find_plane, :only => [:edit, :update, :destroy] + + def index + @planes = Plane.default_order + @title = '位面节点' + end + + def new + @plane = Plane.new + respond_to do |format| + format.js { render :show_form } + end + end + + def create + @plane = Plane.new(params[:plane]) + respond_to do |format| + if @plane.save + format.js + else + format.js { render :show_form } + end + end + end + + def edit + respond_to do |format| + format.js { render :show_form } + end + end + + def update + respond_to do |format| + if @plane.update_attributes(params[:plane]) + format.js { render :js => 'window.location.reload()' } + else + format.js { render :show_form } + end + end + end + + def destroy + respond_to do |format| + if @plane.can_delete? and @plane.destroy + format.js + else + format.js { render :json => {:error => 'delete plane failed'}, :status => :unprocessable_entity } + end + end + end + + def sort + if params[:position].present? + params[:position].each_with_index do |id, pos| + Plane.update(id, :position => pos) + end + + respond_to do |f| + f.js { head :ok } + end + else + respond_to do |f| + f.js { + @planes = Plane.default_order + } + end + end + end +end diff --git a/app/controllers/admin/rewards_controller.rb b/app/controllers/admin/rewards_controller.rb new file mode 100644 index 0000000..1d193d6 --- /dev/null +++ b/app/controllers/admin/rewards_controller.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 +class Admin::RewardsController < Admin::BaseController + before_filter :find_parent_user, :except => [:index] + + def index + @rewards = Reward.order('created_at DESC').page(params[:page]) + @title = '奖励记录' + end + + def new + respond_to do |f| + f.js { + @reward_type = params[:reward_type].present? ? params[:reward_type] : Reward::TYPE_GRANT + @reward = @user.rewards.build(:reward_type => @reward_type, :amount_str => '0') + } + end + end + + def create + respond_to do |f| + f.js { + @reward = @user.rewards.build(params[:reward]) + @reward_type = @reward.reward_type + @reward.admin_user = current_user + + result = Reward.transaction do + @reward.save && @user.update_attributes({:reward => @user.reward + @reward.amount}, :as => current_user.permission_role) + end + + render :new and return unless result + } + end + end +end diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb new file mode 100644 index 0000000..c110c34 --- /dev/null +++ b/app/controllers/admin/site_settings_controller.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 + +class Admin::SiteSettingsController < Admin::BaseController + def show + @settings = Siteconf + @title = '基本设置' + end + + def appearance + @settings = Siteconf + @title = '外观' + end + + def update + params[:settings].each_key do |key| + Siteconf.send("#{key}=", params[:settings][key]) + end + flash[:success] = '保存成功' + redirect_to :back + end +end diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb new file mode 100644 index 0000000..dfaa593 --- /dev/null +++ b/app/controllers/admin/topics_controller.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 +class Admin::TopicsController < Admin::BaseController + def index + @topics = Topic.order('created_at DESC').page(params[:page]) + @title = '讨论话题' + end + + def destroy + @topic = Topic.find(params[:id]) + if @topic.destroy + redirect_to admin_topics_path + else + redirect_to admin_root_path + end + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000..c93fb17 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,88 @@ +# encoding: utf-8 +class Admin::UsersController < Admin::BaseController + before_filter :find_user, :only => [:edit, :update, :toggle_admin, :toggle_blocked, :destroy] + + def index + if params[:nickname].present? + @user = User.find_by_nickname(params[:nickname]) + if @user.present? + @users = [@user] + else + @users = [] + end + @users.pagination_ready(1, @users.size, 10) + else + @users = User.order('created_at DESC, id DESC').page(params[:page]) + end + @title = '用户' + end + + def edit + authorize! :edit_info, @user + + @title = '修改用户信息' + end + + def update + authorize! :edit_info, @user + + if params[:user][:password].empty? + params[:user].delete(:password) + params[:user].delete(:password_confirmation) + end + + if @user.update_attributes(params[:user], :as => current_user.permission_role) + redirect_to admin_users_path + "?nickname=#{@user.nickname}" + else + render :edit + end + end + + def toggle_admin + respond_to do |format| + if current_user.can_manage_site? + if @user.admin? + @user.acts_as_normal_user + else + @user.acts_as_admin + end + if @user.save + format.js + else + format.js { render :json => {:error => 'toggle admin failed'}, :status => :unprocessable_entity } + end + else + format.js { render :json => {:error => 'no permission'}, :status => :forbidden } + end + end + end + + def toggle_blocked + respond_to do |format| + if current_user.can_manage_site? + if @user.toggle!(:blocked) + format.js + else + format.js { render :json => {:error => 'toggle admin failed'}, :status => :unprocessable_entity } + end + else + format.js { render :json => {:error => 'no permission'}, :status => :forbidden } + end + end + end + + def destroy + if @user.destroy + flash[:success] = "会员 #{@user.nickname} 删除成功。" + redirect_to root_path + else + flash[:error] = "会员 #{@user.nickname} 删除失败。" + redirect_to member_path(@user.nickname) + end + end + + private + def find_user + @user = User.find(params[:id]) + end +end diff --git a/app/controllers/admin/welcome_admin_controller.rb b/app/controllers/admin/welcome_admin_controller.rb new file mode 100644 index 0000000..4f33d1c --- /dev/null +++ b/app/controllers/admin/welcome_admin_controller.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 +class Admin::WelcomeAdminController < Admin::BaseController + def index + @title = '运行状态' + @notifications_to_clear = Notification.where(:unread => false).count + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..4de0801 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,127 @@ +# encoding: utf-8 +class ApplicationController < ActionController::Base + protect_from_forgery + include ApplicationHelper + + rescue_from CanCan::AccessDenied do |exception| + exception.default_message = t('tips.no_permission') + redirect_to root_url, :alert => exception.message + end + + rescue_from ActiveRecord::RecordNotFound do |exception| + case request.format.to_sym + when :html, :mobile + @title = '404: Not Found' + @note = '您要访问的页面不存在。' + @exception = exception + render 'welcome/exception' and return + when :js + render :json => {:error => 'record not found'}, :status => :not_found and return + end + end + + rescue_from NoMethodError, RuntimeError do |exception| + logger.error exception.message + exception.backtrace.each { |line| logger.error line } + + case request.format.to_sym + when :html, :mobile + @title = '500: Internal Error' + @note = '不好意思,系统运行遇到了错误。' + @exception = exception + render 'welcome/exception' and return + when :js + render :json => {:error => exception.inspect}, :status => :internal_server_error and return + end + end + + before_filter :init + before_filter :detect_mobile_client + + def custom_path(model) + if model.is_a? Topic + t_path(model.id) + elsif model.is_a? Node + go_path(model.key) + else + model + end + end + + def method_missing(method, *args, &block) + if method =~ /^find_(.*)able/ + such_able = "@#{$1}able" + params.each do |name, value| + if name =~ /(.+)_id$/ + instance_variable_set(such_able.to_sym, $1.classify.constantize.find(value)) and return + end + end + else + super + end + end + + def init + count_unread_notification + initialize_breadcrumbs_and_title + + @seo_description = Siteconf.seo_description + ActionMailer::Base.default_url_options[:host] = Siteconf.site_host + end + + def initialize_breadcrumbs_and_title + unless request.format.to_sym == :js + basic_name = append_notification_count(Siteconf.site_name) + @title_items = [basic_name] + @breadcrumbs = [%(#{Siteconf.site_name})] + end + end + + def mobile_device? + request.format == :mobile + end + + private + # Overwriting the sign_out redirect path method + def after_sign_out_path_for(resource_or_scope) + goodbye_path + end + + def count_unread_notification + unless request.format.to_sym == :js or params[:controller] == 'notifications' + @unread_count = current_user.try(:unread_notification_count) || 0 + else + @unread_count = 0 + end + @show_notification_count = true + end + + # From Teambox + def detect_mobile_client + ua = request.env['HTTP_USER_AGENT'] + session[:posting_device] = ua[/(iPod|iPad|iPhone|Android)/i] if ua.present? + + if [:html, :mobile].include?(request.format.try(:to_sym)) and session[:format] + # Format has been forced by Sessions#change_format + request.format = session[:format].to_sym + else + # We should autodetect mobile clients and redirect if they ask for html + mobile_regex = /(iPod|iPhone|Android|Opera mini|Opera mobi|Blackberry|Palm|UCWEB|Windows CE|PSP|Blazer|iemobile|webOS)/i + mobile = ua && ua[mobile_regex] + mobile ||= request.env["HTTP_PROFILE"] || request.env["HTTP_X_WAP_PROFILE"] + if mobile and request.format == :html + request.format = :mobile + end + end + end + + def store_location + # NOTE: + # This is a hack + # It works as long as devise doesn't change its source code + # See: https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/helpers.rb#L172 + session[:user_return_to] = request.fullpath + end + +end + diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb new file mode 100644 index 0000000..56ea554 --- /dev/null +++ b/app/controllers/bookmarks_controller.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 +class BookmarksController < ApplicationController + before_filter :authenticate_user! + before_filter :find_bookmarkable, :only => :create + + def create + @bookmark = @bookmarkable.bookmarks.build + @bookmark.user = current_user + flash[:error] = '收藏失败' unless @bookmark.save + redirect_to custom_path(@bookmarkable) + end + + def destroy + @bookmark = Bookmark.find(params[:id]) + authorize! :update, @bookmark + if @bookmark.destroy + redirect_to custom_path(@bookmark.bookmarkable) + else + flash[:error] = '取消收藏失败' + redirect_to root_path + end + end + +end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 0000000..cfe2b8d --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 +class CommentsController < ApplicationController + before_filter :authenticate_user! + before_filter :find_commentable, :only => [:create] + before_filter :find_comment_and_auth, :only => [:edit, :update, :destroy] + + def create + redirect_to root_path, :notice => I18n.t('tips.comments_closed') and return if @commentable.try(:comments_closed) + @comment = @commentable.comments.build(params[:comment]) + @comment.user = current_user + @comment.posting_device = session[:posting_device] if session[:posting_device].present? + flash[:else] = '添加回复失败' unless @comment.save + redirect_to custom_path(@commentable) + end + + def edit + respond_to do |format| + format.js + end + end + + def update + respond_to do |format| + if @comment.update_attributes(params[:comment]) + format.js + else + render :json => :error, :status => :unprocessable_entity + end + end + end + + def destroy + respond_to do |format| + if @comment.destroy + format.js + else + render :json => :error, :status => :unprocessable_entity + end + end + end + + private + def find_comment_and_auth + render :text => :error, :status => :not_found and return unless current_user.can_manage_site? + @comment = Comment.find(params[:id]) + end +end diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb new file mode 100644 index 0000000..1c0ea3f --- /dev/null +++ b/app/controllers/nodes_controller.rb @@ -0,0 +1,19 @@ +class NodesController < ApplicationController + def show + @node = Node.find_by_attr_cached!(:key, params[:key]) + @title = @node.name + @page_num = params[:p].nil? ? 1 : params[:p].to_i + @total_topics = @node.topics_count + @total_pages = (@total_topics * 1.0 / Siteconf.pagination_topics.to_i).ceil + @next_page_num = (@page_num < @total_pages) ? @page_num + 1 : 0 + @prev_page_num = (@page_num > 1) ? @page_num - 1 : 0 + @topics = @node.cached_assoc_pagination(:topics, @page_num, Siteconf.pagination_topics.to_i, 'updated_at') + + @canonical_path = "/go/#{params[:key]}?p=#{@page_num}" + + respond_to do |format| + format.html + format.mobile { add_breadcrumb @node.name } + end + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 0000000..b9e880a --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 +class NotificationsController < ApplicationController + before_filter :authenticate_user! + + def index + @notifications = current_user.notifications.where(:unread => true).order('created_at DESC').limit(100).all + current_user.notifications.update_all(:unread => false) + @unread_count = 0 + + @title = '提醒系统' + + respond_to do |format| + format.html + format.mobile { + add_breadcrumb(@title) + @show_notification_count = false + } + end + end + + def read + @notification = current_user.notifications.find(params[:id]) + if @notification.present? + redirect_to @notification.notifiable.notifiable_path + else + redirect_to root_path, :error => '无法处理之前的请求' + end + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 0000000..1daaf68 --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,10 @@ +class PagesController < ApplicationController + def show + if current_user && current_user.can_manage_site? + @page = Page.find_by_key!(params[:key]) + else + @page = Page.find_by_attr_cached!(:key, params[:key], :published => true) + end + @title = @page.title + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..a59efc5 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 +class RegistrationsController < Devise::RegistrationsController + def create + build_resource + + if resource.verify_captcha(session[:captcha]) and resource.save + if resource.active_for_authentication? + set_flash_message :notice, :signed_up if is_navigational_format? + sign_in(resource_name, resource) + respond_with resource, :location => after_sign_up_path_for(resource) + else + set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format? + expire_session_data_after_sign_in! + respond_with resource, :location => after_inactive_sign_up_path_for(resource) + end + else + clean_up_passwords resource + respond_with resource + end + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..a717ccf --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,14 @@ +class SessionsController < Devise::SessionsController + def create + old_session = session.dup + resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new") + sign_in(resource_name, resource) + reset_session + session.reverse_merge!(old_session) + if mobile_device? + redirect_to root_path + else + respond_with resource, :location => after_sign_in_path_for(resource) + end + end +end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb new file mode 100644 index 0000000..4a6b05b --- /dev/null +++ b/app/controllers/topics_controller.rb @@ -0,0 +1,170 @@ +# encoding: utf-8 +class TopicsController < ApplicationController + before_filter :authenticate_user!, :except => [:show, :index] + before_filter :find_node, :except => [:show, :index, :preview, :toggle_comments_closed, :toggle_sticky] + before_filter :find_topic_and_auth, :only => [:edit_title,:update_title, + :edit, :update, :move, :destroy] + before_filter :only => [:toggle_comments_closed, :toggle_sticky] do |c| + auth_admin + end + + def index + respond_to do |format| + format.html { + per_page = Siteconf::HOMEPAGE_TOPICS + current_page = params[:page].present? ? params[:page].to_i : 1 + total_pages = (Topic.cached_count * 1.0 / per_page).ceil + @topics = Topic.cached_pagination(current_page, per_page, 'updated_at') + @topics.pagination_ready(current_page, total_pages, per_page) + @title = '全站最新更改记录' + } + format.atom { + @feed_items = Topic.recent_topics(Siteconf::HOMEPAGE_TOPICS) + @last_update = @feed_items.first.updated_at unless @feed_items.empty? + render :layout => false + } + end + end + + def show + raise ActiveRecord::RecordNotFound.new if params[:id].to_i.to_s != params[:id] + + @topic = Topic.find_cached(params[:id]) + store_location + # NOTE + # We can't use @topic.increment!(:hit) here, + # because updated_at is part of the cache key + ActiveRecord::Base.connection.execute("UPDATE topics SET hit = hit + 1 WHERE topics.id = #{@topic.id}") + + @title = @topic.title + @node = @topic.cached_assoc_object(:node) + + @total_comments = @topic.comments_count + @total_pages = (@total_comments * 1.0 / Siteconf.pagination_comments.to_i).ceil + @current_page = params[:p].nil? ? @total_pages : params[:p].to_i + @per_page = Siteconf.pagination_comments.to_i + @comments = @topic.cached_assoc_pagination(:comments, @current_page, @per_page, 'created_at', Rabel::Model::ORDER_ASC) + + @new_comment = @topic.comments.new + @total_bookmarks = @topic.bookmarks.count + + @canonical_path = "/t/#{params[:id]}" + @canonical_path += "?p=#{@current_page}" if @total_pages > 1 and @current_page != @total_pages + @seo_description = @topic.content.present? ? @topic.content.slice(0, 50) : @topic.title + @seo_description += " - #{@topic.user.nickname}" + + respond_to do |format| + format.html + format.mobile + end + end + + def new + @topic = @node.topics.new + + respond_to do |format| + format.html + format.mobile + end + end + + def create + @topic = @node.topics.new(params[:topic], :as => current_user.permission_role) + @topic.user = current_user + if @topic.save + redirect_to t_path(@topic.id) + else + render :new + end + end + + def edit_title + respond_to do |f| + f.js + end + end + + def update_title + respond_to do |f| + f.js { + unless @topic.update_attributes(params[:topic]) + render :text => :error, :status => :unprocessable_entity + end + } + end + end + + def edit + end + + def update + if params[:new_node_id].present? + # move to new node + @new_node = Node.find(params[:new_node_id]) + respond_to do |format| + format.js { + if @new_node.present? + @topic.node = @new_node + if @topic.save + render :js => "window.location.reload()" + else + render :js => "$.facebox('移动帖子失败')" + end + else + render :js => "$.facebox('节点不存在')" + end + } + end + else + if @topic.update_attributes(params[:topic], :as => current_user.permission_role) + redirect_to t_path(@topic.id) + else + flash[:error] = '之前的更新有误,请编辑后再提交' + render :edit + end + end + end + + def move + respond_to do |format| + format.js + end + end + + def destroy + if @topic.destroy + redirect_to root_path, :notice => '帖子删除成功' + else + raise RuntimeError.new('删除帖子出错') + end + end + + def preview + type = ['topic', 'comment', 'page'].delete params[:type] + render :text => send("format_#{type}".to_sym, params[:content]) if type.present? + end + + def toggle_comments_closed + @topic = Topic.find(params[:topic_id]) + @topic.toggle!(:comments_closed) + @topic.touch + redirect_to t_path(@topic.id) + end + + def toggle_sticky + @topic = Topic.find(params[:topic_id]) + @topic.toggle!(:sticky) + @topic.touch + redirect_to t_path(@topic.id) + end + + private + def find_node + @node = Node.find(params[:node_id]) + end + + def find_topic_and_auth + @topic = @node.topics.find(params[:id]) + authorize! :update, @topic, :message => '你没有权限管理此主题' + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..be1081a --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,117 @@ +# encoding: utf-8 +class UsersController < ApplicationController + before_filter :authenticate_user!, :except => [:show, :topics] + + def show + @user = User.find_by_attr_cached!(:nickname, params[:nickname]) + store_location + + @title = @user.nickname + @canonical_path = "/member/#{@title}" + + @signature = @user.account.signature + @weibo_link = cannonical_url(@user.account.weibo_link) + @personal_website = cannonical_url(@user.account.personal_website) + @location = @user.account.location + @introduction = @user.account.introduction + + @nickname_tip = (@user == current_user) ? '我' : @user.nickname + + respond_to do |format| + format.html + format.mobile { add_breadcrumb @user.nickname } + end + end + + def topics + @user = User.find_by_attr_cached!(:nickname, params[:nickname]) + + @current_page = params[:page].present? ? params[:page] : 1 + @topics = @user.cached_assoc_pagination(:topics, @current_page, 20, 'created_at') + + @title = "#{@user.nickname} 创建的所有主题" + end + + def edit + @user = current_user + @user.build_account unless @user.account.present? + @title = '设置' + + respond_to do |format| + format.html + format.mobile + end + end + + def update_account + @user = current_user + if @user.update_without_password(params[:user]) + flash[:success] = '个人设置成功更新' + sign_in :user, current_user, :bypass => true + redirect_to settings_path + else + flash[:error] = '个人设置保存失败' + render :edit + end + end + + def update_password + @user = current_user + if @user.update_with_password(params[:user]) + flash[:success] = '密码已成功更新,下次请用新密码登录' + sign_in :user, current_user, :bypass => true + redirect_to settings_path + else + flash[:error] = '密码更新失败' + render :edit + end + end + + def update_avatar + @user = current_user + if @user.update_without_password(params[:user]) + flash[:success] = '头像更新成功' + redirect_to settings_path + else + flash[:error] = '头像更新失败' + render :edit + end + end + + def my_topics + @my_topics = current_user.bookmarked_topics + @title = '我收藏的话题' + + respond_to do |format| + format.html + format.mobile { add_breadcrumb(@title) } + end + end + + def my_following + @my_followed_users = current_user.followed_users + @followed_topic_timeline = current_user.followed_topic_timeline + @title = '我的特别关注' + + respond_to do |format| + format.html + format.mobile { add_breadcrumb(@title) } + end + end + + def follow + @followed_user = User.find_by_nickname!(params[:nickname]) + unless current_user.following?(@followed_user) + flash[:error] = '加入特别关注失败' unless current_user.follow(@followed_user) + end + redirect_to member_path(params[:nickname]) + end + + def unfollow + @followed_user = User.find_by_nickname!(params[:nickname]) + if current_user.following?(@followed_user) + flash[:error] = '取消特别关注失败' unless current_user.unfollow(@followed_user) + end + redirect_to member_path(params[:nickname]) + end +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb new file mode 100644 index 0000000..fa244b8 --- /dev/null +++ b/app/controllers/welcome_controller.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 +class WelcomeController < ApplicationController + def index + @topics = Topic.home_topics(Siteconf::HOMEPAGE_TOPICS) + @sticky_topics = Topic.sticky_topics + @canonical_path = '/' + @full_title = site_intro + + respond_to do |format| + format.html + format.mobile { @planes = Plane.all } + end + end + + def goodbye + @title = '登出' + + respond_to do |format| + format.html + format.mobile { add_breadcrumb @title } + end + end + + def captcha + head :ok and return unless Siteconf.show_captcha? + + respond_to do |format| + format.gif { + session[:captcha] = Rabel::Captcha.random_code + send_data Rabel::Captcha.image(session[:captcha]), :type => 'image/gif', :disposition => 'ineline' + } + end + end +end diff --git a/app/helpers/admin/base_helper.rb b/app/helpers/admin/base_helper.rb new file mode 100644 index 0000000..b53154c --- /dev/null +++ b/app/helpers/admin/base_helper.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 +module Admin::BaseHelper + def prepare_resource(resource) + r = [:admin] + if resource.is_a? Array + r += resource + else + r << resource + end + r + end + + def admin_create_button(text, resource, options={}) + default_option = {:class => 'super normal button'} + link_to text, new_polymorphic_path(prepare_resource(resource)), default_option.merge(options) + end + + def admin_edit_button(text, resource, options={}) + default_option = {:class => 'op admin_op'} + link_to text, edit_polymorphic_path(prepare_resource(resource)), default_option.merge(options) + end + + def admin_delete_button(resource, options={}) + default_option = {:class => 'op admin_op op_danger', :method => :delete, :data => {:confirm => '真的要删除吗?'}} + link_to '删除', prepare_resource(resource), default_option.merge(options) + end + + def dashboard_menu + [ + { + :name => '社区管理', + :items => [ + ['运行状态', 'dashboard', admin_root_path], + ['基本设置', 'settings', admin_site_settings_path], + ['外观', 'palette', admin_appearance_path], + ['用户', 'users', admin_users_path], + ['位面节点', 'nodes', admin_planes_path], + ['讨论话题', 'topics', admin_topics_path], + ['页面', 'pages', admin_pages_path], + ['广告位', 'ads', admin_advertisements_path], + ['云硬盘', 'cloud', admin_cloud_files_path], + ['奖励记录', 'reward_history', admin_rewards_path], + ] + } + ] + end + + def page_publish_status(page) + content_tag(:span, page.published? ? '已发布' : '草稿', :class => page.published? ? 'published' : 'draft') + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..55bda2c --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,212 @@ +# encoding: utf-8 +module ApplicationHelper + def site_intro + append_notification_count(Siteconf.site_name + ' - ' + Siteconf.short_intro) + end + + def append_notification_count(title) + return title if @unread_count == 0 + title + " (#{@unread_count})" + end + + def title + return @full_title if @full_title.present? + add_title_item @title if @title.present? + @title_items.join(' - ') + end + + def add_title_item(item) + @title_items.unshift item unless request.format.to_sym == :js + end + + def build_navigation(items, class_name='fade') + items.unshift(link_to(Siteconf.site_name, root_path)) + items.join('  ›  ').html_safe + end + + def add_breadcrumb(item) + @breadcrumbs << item + end + + def build_breadcrumbs + result_html = + case @breadcrumbs.length + when 1 + '' + else + @breadcrumbs.join(' › ') + end + result_html.html_safe + end + + def build_admin_navigation(items, class_name='fade') + items.unshift(link_to('管理后台', admin_root_path)) + build_navigation(items, class_name) + end + + def edit_topic_navigation(node, topic) + build_navigation([ + link_to(node.name, go_path(node.key)), + link_to(topic.title, t_path(topic.id)), + '编辑' + ], 'bigger') + end + + def format_page(page_content) + if Siteconf.allow_markdown_in_pages? + parse_markdown(page_content) + else + format_content(page_content) + end + end + + def format_topic(topic_content) + if Siteconf.allow_markdown_in_topics? + parse_markdown(topic_content) + else + format_content(topic_content) + end + end + + def format_comment(comment_content) + if Siteconf.allow_markdown_in_comments? + parse_markdown(comment_content) + else + format_content(comment_content) + end + end + + def format_content(text) + begin + text = Rabel::LinkEmailParser.parse_url(Rabel::Base.h(text)) do |link| + Rabel::Base.smart_url(link) + end + text = Rabel::LinkEmailParser.parse_email(text) do |address| + Rabel::Base.protect_at_symbol(address) + end + + nl_to_br(Rabel::Base.decode_symbols(Rabel::Base.make_mention_links(text))).html_safe + rescue + h(text) + end + end + + def nl_to_br(text) + text.gsub("\r\n", "
").gsub("\r", "
").gsub("\n", "
") + end + + def parse_markdown(text) + begin + nl_to_br(Rabel::Base.decode_symbols( + Rabel::Base.make_mention_links( + MarkdownConverter.convert(text) + ) + )).html_safe + rescue + h(text) + end + end + + def flash_messages + @flash_messages ||= flash.select {|type, message| message.length > 0} + end + + def show_flash_messages + result = [] + flash_messages.each do |type, message| + result << content_tag(:span, message, :class => "#{type}-message") + end + result.join('
').html_safe + end + + def show_mobile_messages + if flash_messages.any? + content_tag(:div, show_flash_messages, :class => :cell) + end + end + + def search_engines + { + :google => 'http://www.google.com.hk/search?q=', + :bing => 'http://cn.bing.com/search?q=', + :baidu => 'http://www.baidu.com/s?wd=' + } + end + + def search_engine_url + search_engines[Siteconf.default_search_engine.to_sym] + end + + def large_avatar(user) + image_tag user.avatar.url, :class => :large_avatar, :alt => "#{user.nickname} large avatar" + end + + def medium_avatar(user) + image_tag user.avatar.url(:medium), :class => :medium_avatar, :alt => "#{user.nickname} medium avatar" + end + + def mini_avatar(user) + image_tag user.avatar.url(:mini), :class => :mini_avatar, :alt => "#{user.nickname} mini avatar" + end + + def hash_key_append(hash, key, value) + if hash[key].present? + hash[key] = "#{hash[key]} #{value}" + else + hash[key] = value + end + end + + def nickname_profile_link(nickname, options={}) + options[:title] = nickname + hash_key_append(options, :class, 'profile_link') + + link_to nickname, member_path(url_encode(nickname)), options + end + + def user_profile_link(user, options={}) + nickname_profile_link(user.nickname, options) + end + + def user_profile_avatar_link(user, avatar_size, options={}) + avatar_method = "#{avatar_size}_avatar" + + options[:title] = user.nickname + hash_key_append(options, :class, 'profile_link') + + link_to(member_path(url_encode(user.nickname)), options) { send(avatar_method, user) } + end + + def page_real_url(page) + if page.content.start_with?('http') + page.content + elsif page.content.start_with?('/') + page.content + else + page_path(page.key) + end + end + + def show_posting_device(comment) + content_tag(:span, "  via #{comment.posting_device}".html_safe, class: :snow) if comment.posting_device.present? + end + + def auth_admin(error_tip='tips.no_permission') + redirect_to root_path, :notice => t(error_tip) unless current_user.can_manage_site? + end + + def cannonical_url(url) + return url if url.length == 0 + url.start_with?('http://') ? url : 'http://' + url + end + + def weibo_icon_for(weibo_link) + if weibo_link.include?('weibo.com') + 'sina_weibo' + elsif weibo_link.include?('t.qq.com') + 'tx_weibo' + else + 'twitter' + end + end +end diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/.gitkeep b/app/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..e91eed9 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,42 @@ +class Ability + include CanCan::Ability + + def initialize(user) + user ||= User.new + + can :update, Topic do |topic| + topic.allow_modification_by?(user) + end + + can :update, Bookmark do |bookmark| + bookmark.user == user + end + + can :edit_info, User do |target_user| + user.root? or (user.can_manage_site? and (not target_user.root?)) + end + + # Define abilities for the passed in user here. For example: + # + # user ||= User.new # guest user (not logged in) + # if user.admin? + # can :manage, :all + # else + # can :read, :all + # end + # + # The first argument to `can` is the action you are giving the user permission to do. + # If you pass :manage it will apply to every action. Other common actions here are + # :read, :create, :update and :destroy. + # + # The second argument is the resource the user can perform the action on. If you pass + # :all it will apply to every resource. Otherwise pass a Ruby class of the resource. + # + # The third argument is an optional hash of conditions to further filter the objects. + # For example, here the user can only update published articles. + # + # can :update, Article, :published => true + # + # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities + end +end diff --git a/app/models/account.rb b/app/models/account.rb new file mode 100644 index 0000000..90029dc --- /dev/null +++ b/app/models/account.rb @@ -0,0 +1,20 @@ +class Account < ActiveRecord::Base + belongs_to :user + + BASE_FIELDS = [:personal_website, :location, :signature, :introduction, :weibo_link] + attr_accessible *BASE_FIELDS + attr_accessible *BASE_FIELDS, :as => :admin + + validates :signature, :length => {:maximum => 20} + + before_create :set_default_value + + private + def set_default_value + self.personal_website ||= '' + self.location ||= '' + self.signature ||= '' + self.introduction ||= '' + end + +end diff --git a/app/models/advertisement.rb b/app/models/advertisement.rb new file mode 100644 index 0000000..afee4b5 --- /dev/null +++ b/app/models/advertisement.rb @@ -0,0 +1,34 @@ +class Advertisement < ActiveRecord::Base + validates :link, :banner, :title, :words, :start_date, :expire_date, :duration, :presence => true + validates :duration, :numericality => {:only_integer => true, :less_than => 3650, :greater_than => 1} + + attr_accessible :link, :banner, :title, :words, :start_date, :duration + mount_uploader :banner, AdBannerUploader + before_validation :set_expire_date + + def self.available + num = available_condition.count + return Rabel::Model::EMPTY_DATASET if num == 0 + ts = select('updated_at').order('updated_at DESC').first.try(:updated_at) + Rails.cache.fetch("#{self.model_name.collection}/available/#{num}-#{ts}") do + available_condition.order('start_date DESC').all + end + end + + def self.available_condition + today = Time.zone.today + where(["start_date <= ? AND expire_date >= ?", today, today]) + end + + def showing? + today = Time.zone.today + today >= self.start_date and today <= self.expire_date + end + + private + def set_expire_date + if self.start_date.present? and self.duration.present? + self.expire_date = self.start_date + self.duration.days + end + end +end diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb new file mode 100644 index 0000000..ef26695 --- /dev/null +++ b/app/models/bookmark.rb @@ -0,0 +1,7 @@ +class Bookmark < ActiveRecord::Base + validates :user_id, :bookmarkable_type, :bookmarkable_id, :presence => true + attr_protected :user_id, :bookmarkable_type, :bookmarkable_id + + belongs_to :user + belongs_to :bookmarkable, :polymorphic => true +end diff --git a/app/models/cloud_file.rb b/app/models/cloud_file.rb new file mode 100644 index 0000000..7773c08 --- /dev/null +++ b/app/models/cloud_file.rb @@ -0,0 +1,14 @@ +class CloudFile < ActiveRecord::Base + attr_accessible :name, :asset + + mount_uploader :asset, CloudFileUploader + + validates :name, :asset, :presence => true + + before_create :save_metadata + private + def save_metadata + self.content_type = asset.file.content_type + self.file_size = asset.file.size + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..1d85547 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,77 @@ +class Comment < ActiveRecord::Base + include Rabel::ActiveCache + + belongs_to :user + belongs_to :commentable, :polymorphic => true, :counter_cache => true + + validates :user_id, :commentable_id, :commentable_type, :content, :presence => true + attr_accessible :content + + after_create :touch_parent_model + after_create :send_notifications + after_destroy :update_last_reply + + def mentioned_users + mentioned_names = self.content.scan(Notifiable::MENTION_REGEXP).collect {|matched| matched.first}.uniq + mentioned_names.delete(self.user.nickname) + mentioned_names.delete(self.commentable.user.nickname) + mentioned_names.map { |name| User.find_by_nickname(name) }.compact + end + + private + def touch_parent_model + created_date = commentable.created_at.to_date + if commentable.has_attribute?(:last_replied_by) and commentable.has_attribute?(:last_replied_at) + commentable.last_replied_by = self.user.nickname + commentable.last_replied_at = self.created_at + commentable.save + end + + if commentable.has_attribute?(:involved_at) + commentable.touch(:involved_at) if created_date.months_since(6) > Time.zone.today + else + commentable.touch + end + end + + def send_notifications + # send notification to commentable owner + # unless the comment was created by the same owner + send_notification_to( + self.commentable.user, + Notification::ACTION_REPLY) unless self.user == self.commentable.user + send_notification_to_mentioned_users + end + + def send_notification_to(user, action) + Notification.notify( + user, + self.commentable, + self.user, + action, + self.content + ) + end + + def send_notification_to_mentioned_users + mentioned_users.each do |user| + send_notification_to(user, Notification::ACTION_MENTION) + end + end + + def update_last_reply + if commentable.has_attribute?(:last_replied_by) and commentable.has_attribute?(:last_replied_at) + if commentable.comments_count == 0 + commentable.last_replied_by = '' + commentable.last_replied_at = '' + else + last_comment = commentable.try(:last_comment) + if last_comment.present? + commentable.last_replied_by = last_comment.user.nickname + commentable.last_replied_at = last_comment.created_at + end + end + commentable.save + end + end +end diff --git a/app/models/following.rb b/app/models/following.rb new file mode 100644 index 0000000..8646cb5 --- /dev/null +++ b/app/models/following.rb @@ -0,0 +1,9 @@ +# FIXME +# Three indexes were added: :user_id, :followed_user_id and [:user_id, :followed_user_id] +# Need to know if all indexes are needed +class Following < ActiveRecord::Base + attr_accessible :followed_user_id + + belongs_to :follower, :class_name => 'User', :foreign_key => 'user_id' + belongs_to :followed_user, :class_name => 'User' +end diff --git a/app/models/node.rb b/app/models/node.rb new file mode 100644 index 0000000..a043184 --- /dev/null +++ b/app/models/node.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 +class Node < ActiveRecord::Base + include Sortable + include Rabel::ActiveCache + + has_many :topics + has_many :bookmarks, :as => :bookmarkable, :dependent => :destroy + belongs_to :plane, :touch => true + + validates :name, :plane_id, :key, :presence => true + validates :key, :uniqueness => true, :format => {:with => /[a-zA-Z0-9_-]+/, :message => I18n.t('tips.node_key_format')} + validate :node_key_should_not_contain_slash + + attr_accessible :plane_id, :name, :key, :custom_css, :custom_html, :introduction, :position, :quiet + + def can_delete? + self.topics_count == 0 + end + + private + def node_key_should_not_contain_slash + errors.add(:key, "不能包含斜线(/)") if self.key.present? and self.key.include?('/') + end +end diff --git a/app/models/notifiable.rb b/app/models/notifiable.rb new file mode 100644 index 0000000..1b60a68 --- /dev/null +++ b/app/models/notifiable.rb @@ -0,0 +1,16 @@ +module Notifiable + MENTION_REGEXP = /@([a-zA-Z0-9_\-\p{han}]+)/u + + def notifiable_title + default_implementation + end + + def notifiable_path + default_implementation + end + + private + def default_implementation + raise "Notifiable callback was not implemented." + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..2585f3e --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 +class Notification < ActiveRecord::Base + ACTION_MENTION = 'mention' + ACTION_REPLY = 'reply' + ACTION_TOPIC = 'topic' + ACTION_REWARD = 'reward' + + belongs_to :user + belongs_to :action_user, :class_name => 'User' + belongs_to :notifiable, :polymorphic => true + + attr_accessible :content, :action + + # Notify user + def self.notify(user, notifiable, action_user, action, content) + nf = Notification.new(:action => action, :content => content) + nf.notifiable = notifiable + nf.user = user + nf.action_user = action_user + nf.save + end + + def action_info_prefix + case self.action + when ACTION_MENTION + '在回复' + when ACTION_REPLY + '在' + when ACTION_TOPIC + '在话题' + end + end + + def action_info_suffix + case self.action + when ACTION_MENTION + '时提到' + when ACTION_REPLY + '里回复' + when ACTION_TOPIC + '中提到' + end + end +end diff --git a/app/models/page.rb b/app/models/page.rb new file mode 100644 index 0000000..176b5d1 --- /dev/null +++ b/app/models/page.rb @@ -0,0 +1,14 @@ +# encoding: utf-8 +class Page < ActiveRecord::Base + include Sortable + include Rabel::ActiveCache + + acts_as_list + + validates :key, :title, :content, :presence => true + attr_accessible :key, :title, :content, :published, :position + + def self.only_published + where(:published => true) + end +end diff --git a/app/models/plane.rb b/app/models/plane.rb new file mode 100644 index 0000000..e07d195 --- /dev/null +++ b/app/models/plane.rb @@ -0,0 +1,13 @@ +class Plane < ActiveRecord::Base + include Rabel::ActiveCache + include Sortable + + validates :name, :presence => true + has_many :nodes, :order => Node.default_order_str + + attr_accessible :name, :position + + def can_delete? + self.nodes.count == 0 + end +end diff --git a/app/models/reward.rb b/app/models/reward.rb new file mode 100644 index 0000000..89aa691 --- /dev/null +++ b/app/models/reward.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 +class Reward < ActiveRecord::Base + TYPE_GRANT = 'grant' + TYPE_REVOKE = 'revoke' + + attr_accessor :amount_str, :reward_type + attr_accessible :reason, :amount, :amount_str, :reward_type + + validates :user_id, :admin_user_id, :amount, :reason, :presence => true + validates :reward_type, :amount_str, :presence => true, :on => :create + validates :reward_type, :inclusion => { :in => [TYPE_GRANT, TYPE_REVOKE] }, :on => :create + + validate :amount_rules, :on => :create + + belongs_to :user + belongs_to :admin_user, :class_name => 'User' + + before_validation :set_amount + before_create :set_balance + after_create :notify_user + + private + def set_amount + if self.reward_type == TYPE_GRANT + self.amount = self.amount_str.to_i.abs + else + self.amount = self.amount_str.to_i.abs * -1 + end + end + + def set_balance + self.balance = self.user.reward + self.amount + end + + def notify_user + Notification.notify( + self.user, + self, + self.admin_user, + Notification::ACTION_REWARD, + self.reason + ) + end + + def amount_rules + if self.amount.present? + if self.amount == 0 + errors.add(:amount_str, "不能为零") + end + + if self.reward_type == TYPE_REVOKE and self.amount.abs > self.user.reward + errors.add(:amount_str, "扣除金额不能超过可用余额") + end + end + end +end diff --git a/app/models/settings.rb b/app/models/settings.rb new file mode 100644 index 0000000..415bf20 --- /dev/null +++ b/app/models/settings.rb @@ -0,0 +1,13 @@ +# encoding: utf-8 +class Settings < Settingslogic + source "#{Rails.root}/config/settings.yml" + namespace Rails.env + + def self.themes + {:default => '默认', :ruby_china => 'Ruby China'} + end + + def self.topic_list_styles + {:simple => '极简', :complex => '丰富'} + end +end diff --git a/app/models/siteconf.rb b/app/models/siteconf.rb new file mode 100644 index 0000000..706f036 --- /dev/null +++ b/app/models/siteconf.rb @@ -0,0 +1,57 @@ +class Siteconf < RailsSettings::CachedSettings + def self.boolean_attributes(*args) + args.each do |m| + self.instance_eval <<-CODE + def #{m}? + Siteconf.send(:#{m}) == 'on' + end + CODE + end + end + + HOMEPAGE_TOPICS = 15 + attr_accessible :var + + boolean_attributes :show_captcha, :show_community_stats, + :allow_markdown_in_topics, + :allow_markdown_in_comments, + :allow_markdown_in_pages + + class << self + def seo_keywords_str + self.seo_keywords.join(',') + end + + def seo_keywords_str=(str) + self.seo_keywords = str.split(',') + end + + def marketing_str + self.marketing.join(',') + end + + def marketing_str=(str) + self.marketing = str.split(',') + end + + def nav_position_top? + self.nav_position == 'top' + end + + def nav_position_sidebar? + self.nav_position == 'sidebar' + end + + def nav_position_bottom? + self.nav_position == 'bottom' + end + + def topic_editable_period + self.topic_editable_period_str.to_i.minutes + end + + def simple_topic_list_style? + self.topic_list_style == 'simple' + end + end +end diff --git a/app/models/sortable.rb b/app/models/sortable.rb new file mode 100644 index 0000000..0da86b8 --- /dev/null +++ b/app/models/sortable.rb @@ -0,0 +1,23 @@ +module Sortable + def self.included(base) + base.class_eval do + extend ClassMethods + + after_create :set_default_position + private + def set_default_position + self.update_column(:position, id) + end + end + end + + module ClassMethods + def default_order + order(default_order_str) + end + + def default_order_str + 'position ASC' + end + end +end diff --git a/app/models/topic.rb b/app/models/topic.rb new file mode 100644 index 0000000..7dce615 --- /dev/null +++ b/app/models/topic.rb @@ -0,0 +1,108 @@ +class Topic < ActiveRecord::Base + include Notifiable + include Rabel::ActiveCache + + DEFAULT_HIT = 0 + default_value_for :hit, DEFAULT_HIT + default_value_for :content, '' + default_value_for :involved_at do + Time.zone.now + end + + belongs_to :node, :touch => true, :counter_cache => true + belongs_to :user + has_many :comments, :as => :commentable, :dependent => :destroy + has_many :bookmarks, :as => :bookmarkable, :dependent => :destroy + has_many :notifications, :as => :notifiable, :dependent => :destroy + + validates :node_id, :user_id, :title, :presence => true + + attr_accessible :title, :content + attr_accessible :title, :content, :comments_closed, :sticky, :as => :admin + + after_create :send_notifications + + def last_comment + self.comments.order('created_at ASC').last + end + + def locked? + Time.now - self.created_at > Siteconf.topic_editable_period + end + + def allow_modification_by?(user) + (!locked? && self.user == user) || user.can_manage_site? + end + + def notifiable_title + title + end + + def notifiable_path + "/t/#{id}" + end + + def self.sticky_topics + ts = select('updated_at').with_sticky(true).order('updated_at DESC').first.try(:updated_at) + return Rabel::Model::EMPTY_DATASET unless ts.present? + count = with_sticky(true).count + Rails.cache.fetch("topics/sticky/#{ts}-#{count}") do + with_sticky(true).order('updated_at DESC').all + end + end + + def self.home_topics(num) + ts = select('updated_at').order('updated_at DESC').first.try(:updated_at) + return Rabel::Model::EMPTY_DATASET unless ts.present? + node_ts = Node.select('updated_at').order('updated_at DESC').first.try(:updated_at) + Rails.cache.fetch("topics/homepage/#{self.count}/#{num}-#{ts}/#{node_ts}") do + excluded_nodes = Node.where(:quiet => true).pluck(:id) + if excluded_nodes.any? + where("node_id NOT in (?)", excluded_nodes).with_sticky(false).latest_involved_topics(num) + else + with_sticky(false).latest_involved_topics(num) + end + end + end + + def self.with_sticky(sticky) + where(:sticky => sticky) + end + + def self.latest_involved_topics(num) + order('involved_at DESC').limit(num).all + end + + def self.recent_topics(num) + ts = select('updated_at').order('updated_at DESC').first.try(:updated_at) + return Rabel::Model::EMPTY_DATASET unless ts.present? + Rails.cache.fetch("topics/recent/#{self.count}/#{num}-#{ts}") do + order('involved_at DESC').limit(num).all + end + end + + def mention_check_text + self.title + self.content + end + + def mentioned_users + mentioned_names = self.mention_check_text.scan(Notifiable::MENTION_REGEXP).collect {|matched| matched.first}.uniq + mentioned_names.delete(self.user.nickname) + mentioned_names.map { |name| User.find_by_nickname(name) }.compact + end + + private + + def send_notifications + mentioned_users.each do |user| + Notification.notify( + user, + self, + self.user, + Notification::ACTION_TOPIC, + self.content + ) + end + end + +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..3cdf2b5 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,190 @@ +# encoding:utf-8 +require 'carrierwave/orm/activerecord' + +class User < ActiveRecord::Base + include Rabel::ActiveCache + # Include default devise modules. Others available are: + # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :trackable, :validatable + # Setup accessible (or protected) attributes for your model + attr_accessor :captcha + + BASE_FIELDS = [:nickname, :email, :password, :password_confirmation, + :remember_me, :avatar, :account_attributes, :captcha + ] + + attr_accessible *BASE_FIELDS + attr_accessible *(BASE_FIELDS + [:reward]), :as => :admin + + mount_uploader :avatar, AvatarUploader + + validates :nickname, :presence => true, :uniqueness => true, :length => {:maximum => 12} + validate :nickname_cannot_contain_invalid_characters + + has_one :account, :dependent => :destroy + accepts_nested_attributes_for :account + + has_many :topics, :dependent => :destroy + has_many :comments, :dependent => :destroy + has_many :bookmarks, :dependent => :destroy + has_many :notifications, :dependent => :destroy + has_many :rewards + + has_many :follower_relationships, :class_name => 'Following', :foreign_key => 'followed_user_id', :dependent => :destroy + has_many :followed_relationships, :class_name => 'Following', :foreign_key => 'user_id', :dependent => :destroy + + has_many :followers, :through => :follower_relationships + has_many :followed_users, :through => :followed_relationships + + before_create :create_acount + + def latest_created_topics + self.topics.order('created_at DESC').limit(10) + end + + def latest_replied_topics + @topic_ids ||= self.comments.where(:commentable_type => 'Topic').order('created_at DESC').limit(10).pluck(:commentable_id) + Topic.find(@topic_ids) + end + + def bookmarked?(bookmarkable) + bookmarkable.bookmarks.where(:user_id => self.id).exists? + end + + def bookmark_of(bookmarkable) + bookmarkable.bookmarks.where(:user_id => self.id).first + end + + def bookmarked_nodes_count + self.bookmarks.where(:bookmarkable_type => 'Node').count + end + + def bookmarked_nodes + ids = self.bookmarks.select(:bookmarkable_id).where(:bookmarkable_type => 'Node').collect(&:bookmarkable_id) + Node.find(ids) + end + + def bookmarked_topics_count + self.bookmarks.where(:bookmarkable_type => 'Topic').count + end + + def bookmarked_topics + ids = self.bookmarks.select(:bookmarkable_id).where(:bookmarkable_type => 'Topic').collect(&:bookmarkable_id) + Topic.find(ids) + end + + def recent_followers + follower_ids = self.follower_relationships.order('created_at DESC').limit(10).pluck(:user_id) + follower_ids.map { |uid| User.find_cached(uid) } + end + + def follow(user) + self.followed_users << user and true + end + + def unfollow(user) + user.followers.delete(self) and true + end + + def following?(user) + self.followed_relationships.where(:followed_user_id => user.id).exists? + end + + def followed_by?(user) + self.follower_relationships.where(:user_id => user.id).exists? + end + + def follower_count + @follower_count ||= self.follower_relationships.count + end + + def followed_user_count + @followed_user_count ||= self.followed_relationships.count + end + + def follower_ids + @follower_ids ||= self.follower_relationships.collect(&:user_id) + end + + def followed_user_ids + @followed_user_ids ||= self.followed_relationships.collect(&:followed_user_id) + end + + def followed_topic_timeline + # FIXME: cache this result + Topic.where(:user_id => self.followed_user_ids).order('created_at DESC').limit(10) + end + + def root? + self.id == 1 || self.role == 'root' + end + + def permission_role + can_manage_site? ? :admin : :default + end + + def admin? + self.role == 'admin' + end + + def acts_as_admin + self.role = 'admin' + end + + def acts_as_normal_user + self.role = 'user' + end + + def can_manage_site? + root? || admin? + end + + def unread_notification_count + self.notifications.where(:unread => true).count + end + + def active_for_authentication? + super && not_blocked? + end + + def not_blocked? + not self.blocked? + end + + def inactive_message + not_blocked? ? super : :blocked + end + + def verify_captcha(correct_captcha) + return true unless Siteconf.show_captcha? + if self.captcha.downcase == correct_captcha.downcase + true + else + self.errors.add(:captcha, "验证码不正确") + false + end + end + + def has_avatar? + self.read_attribute(:avatar).present? + end + + private + def create_acount + self.build_account if self.account.nil? + end + + def nickname_cannot_contain_invalid_characters + if self.nickname.present? and (self.nickname.include?('@') or + self.nickname.include?('-') or + self.nickname.include?(' ') or + self.nickname.include?('.') or + self.nickname.include?('/') or + self.nickname.include?('\\') + ) + errors.add(:nickname, "不能包含@, 横线, 斜线, 句点或空格") + end + end +end + diff --git a/app/uploaders/ad_banner_uploader.rb b/app/uploaders/ad_banner_uploader.rb new file mode 100644 index 0000000..e254084 --- /dev/null +++ b/app/uploaders/ad_banner_uploader.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +class AdBannerUploader < CarrierWave::Uploader::Base + include UploaderHelper + include PictureExtensionWhiteList + include CarrierWave::MimeTypes + process :set_content_type + + # Include RMagick or MiniMagick support: + include CarrierWave::RMagick + # include CarrierWave::MiniMagick + + # Choose what kind of storage to use for this uploader: + storage :file + # storage :fog + + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + #def store_dir + # "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + #end + + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + "/banner/default.gif" + end + + # Process files as they are uploaded: + process :resize_to_fit => [120, 90] + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end + + # Create different versions of your uploaded files: + # version :thumb do + # process :scale => [50, 50] + # end + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + # def extension_white_list + # %w(jpg jpeg gif png) + # end + + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + +end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb new file mode 100644 index 0000000..1c57f2d --- /dev/null +++ b/app/uploaders/avatar_uploader.rb @@ -0,0 +1,58 @@ +# encoding: utf-8 +class AvatarUploader < CarrierWave::Uploader::Base + include UploaderHelper + include PictureExtensionWhiteList + include CarrierWave::MimeTypes + process :set_content_type + + # Include RMagick or MiniMagick support: + include CarrierWave::RMagick + # include CarrierWave::MiniMagick + + # Choose what kind of storage to use for this uploader: + storage :file + # storage :fog + + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + # def store_dir + # "uploads/#{model.class.to_s.underscore}_#{mounted_as}/#{model.id % 100}/#{model.id % 1000}" + # end + + + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + "/avatar/" + [version_name, "default.png"].compact.join('_') + end + + # Process files as they are uploaded: + process :resize_to_fit => [72, 72] + # + # def scale(width, height) + # # do something + # end + + # Create different versions of your uploaded files: + version :medium do + process :resize_to_fit => [48, 48] + end + + version :mini do + process :resize_to_fit => [24, 24] + end + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + # def extension_white_list + # %w(jpg jpeg gif png) + # end + + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # if original_filename.present? + # hashed_name = Digest::MD5.hexdigest(original_filename)[5..10] + # "#{hashed_name}.#{file.extension}" + # end + # end +end diff --git a/app/uploaders/cloud_file_uploader.rb b/app/uploaders/cloud_file_uploader.rb new file mode 100644 index 0000000..2ee2cad --- /dev/null +++ b/app/uploaders/cloud_file_uploader.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 + +class CloudFileUploader < CarrierWave::Uploader::Base + include UploaderHelper + + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + # include CarrierWave::MiniMagick + + # Choose what kind of storage to use for this uploader: + storage :file + # storage :fog + + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + + # Provide a default URL as a default if there hasn't been a file uploaded: + # def default_url + # "/images/fallback/" + [version_name, "default.png"].compact.join('_') + # end + + # Process files as they are uploaded: + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end + + # Create different versions of your uploaded files: + # version :thumb do + # process :scale => [50, 50] + # end + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + # def extension_white_list + # %w(jpg jpeg gif png) + # end + + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + +end diff --git a/app/uploaders/picture_extension_white_list.rb b/app/uploaders/picture_extension_white_list.rb new file mode 100644 index 0000000..7fbf9c0 --- /dev/null +++ b/app/uploaders/picture_extension_white_list.rb @@ -0,0 +1,5 @@ +module PictureExtensionWhiteList + def extension_white_list + %w(jpg jpeg gif png) + end +end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb new file mode 100644 index 0000000..445b3e6 --- /dev/null +++ b/app/uploaders/uploader_helper.rb @@ -0,0 +1,19 @@ +require 'digest' +require 'carrierwave/processing/mime_types' + +module UploaderHelper + def store_dir + "uploads/#{model.class.to_s.underscore}_#{mounted_as}/#{model.id % 100}/#{model.id % 1000}" + end + + def cache_dir + "/tmp" + end + + def filename + if original_filename.present? + hashed_name = Digest::MD5.hexdigest(File.dirname(current_path))[5..15] + "#{hashed_name}.#{file.extension}" + end + end +end diff --git a/app/views/admin/advertisements/_advertisement.html.haml b/app/views/admin/advertisements/_advertisement.html.haml new file mode 100644 index 0000000..fb95c4b --- /dev/null +++ b/app/views/admin/advertisements/_advertisement.html.haml @@ -0,0 +1,22 @@ +.advertisement + .fr + = admin_edit_button('修改', advertisement) + = render 'shared/ad', :ad => advertisement + .inner + %span.fade 开始日期 + = advertisement.start_date + .sep5 + %span.fade 结束日期 + = advertisement.expire_date + .sep5 + %span.fade 持续时间 + %strong= advertisement.duration + %span.fade 天 + .sep10 + - if advertisement.showing? + %strong.green 展示中 + - elsif advertisement.expire_date < today + %em.red 已过期 + - else + %em 已预约 + = admin_delete_button(advertisement) diff --git a/app/views/admin/advertisements/_form.html.haml b/app/views/admin/advertisements/_form.html.haml new file mode 100644 index 0000000..c544056 --- /dev/null +++ b/app/views/admin/advertisements/_form.html.haml @@ -0,0 +1,58 @@ += form_for [:admin, ad], :html => {:multipart => true} do |f| + %table + %tr + %td + = f.label :title, '广告标题' + %small.smaller 尽可能简短 (必填) + %tr + %td + = f.text_field :title, :class => :sl + .sep10 + %tr + %td + = f.label :words, '广告语' + %small.smaller 最好为一句话 (必填) + %tr + %td + = f.text_area :words, :class => :mlt + .sep10 + %tr + %td + = f.label :link, '广告链接' + %small.smaller (必填) + %tr + %td + = f.text_field :link, :class => :sll + .sep10 + %tr + %td + = f.label :banner, '宣传图片' + %small.smaller (必填) + %tr + %td + = image_tag f.object.banner.url + %tr + %td + = f.file_field :banner + .sep10 + %tr + %td + = f.label :start_date, '开始日期' + %small.smaller (必填) + %tr + %td + = f.text_field :start_date, :class => 'sls datepicker' + .sep10 + %tr + %td + = f.label :duration, '持续时间' + %small.smaller (必填) + %tr + %td + = f.text_field :duration, :class => :sls, :style => 'text-align: right; width: 4em;' + %small.smaller 天 + .sep10 + %tr + %td + = f.submit '保存', :class => 'super normal button' + diff --git a/app/views/admin/advertisements/edit.html.haml b/app/views/admin/advertisements/edit.html.haml new file mode 100644 index 0000000..0d1b24d --- /dev/null +++ b/app/views/admin/advertisements/edit.html.haml @@ -0,0 +1,5 @@ +.box + .cell + = build_admin_navigation([link_to('广告位', admin_advertisements_path), @title]) + .inner + = render 'form', :ad => @ad diff --git a/app/views/admin/advertisements/index.html.haml b/app/views/admin/advertisements/index.html.haml new file mode 100644 index 0000000..6312737 --- /dev/null +++ b/app/views/admin/advertisements/index.html.haml @@ -0,0 +1,13 @@ +.box + .cell + .fr + = admin_create_button('添加新广告', :advertisement) + = build_admin_navigation([@title]) + .cell{:align => :center} + = paginate @ads + .cell + = render @ads, :today => Time.zone.today + .c + .inner{:align => :center} + = paginate @ads + diff --git a/app/views/admin/advertisements/new.html.haml b/app/views/admin/advertisements/new.html.haml new file mode 100644 index 0000000..0d1b24d --- /dev/null +++ b/app/views/admin/advertisements/new.html.haml @@ -0,0 +1,5 @@ +.box + .cell + = build_admin_navigation([link_to('广告位', admin_advertisements_path), @title]) + .inner + = render 'form', :ad => @ad diff --git a/app/views/admin/cloud_files/_cloud_file.html.haml b/app/views/admin/cloud_files/_cloud_file.html.haml new file mode 100644 index 0000000..941c7f7 --- /dev/null +++ b/app/views/admin/cloud_files/_cloud_file.html.haml @@ -0,0 +1,9 @@ +%tr.highlight + %td= cloud_file.name + %td{:align => :center} + = text_field_tag '', cloud_file.asset.url, :class => :sl + = link_to '下载', cloud_file.asset.url, :target => :_blank, :class => 'op admin_op' + %td{:align => :right}= number_to_human_size(cloud_file.file_size) + %td{:align => :right}= cloud_file.content_type + %td{:align => :center} + = admin_delete_button(cloud_file) diff --git a/app/views/admin/cloud_files/_form.html.haml b/app/views/admin/cloud_files/_form.html.haml new file mode 100644 index 0000000..f023726 --- /dev/null +++ b/app/views/admin/cloud_files/_form.html.haml @@ -0,0 +1,16 @@ += form_for [:admin, @file], :multipart => true do |f| + %table + %tr + %td + %td + = f.file_field :asset + %tr + %td + = f.label :name, '简要描述' + %td + = f.text_field :name, :class => :sl + %small.smaller (必填) + %tr + %td + %td + = f.submit '上传', :class => 'super normal button' diff --git a/app/views/admin/cloud_files/index.html.haml b/app/views/admin/cloud_files/index.html.haml new file mode 100644 index 0000000..efed226 --- /dev/null +++ b/app/views/admin/cloud_files/index.html.haml @@ -0,0 +1,18 @@ +.box + .cell + .fr + = admin_create_button('上传文件', :cloud_file) + = build_admin_navigation([@title]) + .cell + %table{:width => '100%', :cellpadding => 5} + %thead + %tr + %th{:align => :left} 简要描述 + %th 访问路径 + %th{:align => :right} 大小 + %th{:align => :right} 文件类型 + %th{:align => :center} 操作 + %tbody + = render @files + .inner + = paginate @files diff --git a/app/views/admin/cloud_files/new.html.haml b/app/views/admin/cloud_files/new.html.haml new file mode 100644 index 0000000..89fb05b --- /dev/null +++ b/app/views/admin/cloud_files/new.html.haml @@ -0,0 +1,5 @@ +.box + .cell + = build_admin_navigation([link_to('云硬盘', admin_cloud_files_path), @title]) + .inner + = render 'form' diff --git a/app/views/admin/nodes/_form.html.haml b/app/views/admin/nodes/_form.html.haml new file mode 100644 index 0000000..59096cf --- /dev/null +++ b/app/views/admin/nodes/_form.html.haml @@ -0,0 +1,49 @@ +%h2= "修改 #{node.name} 节点" += form_for [:admin, plane, node], :remote => true do |f| + %table + %tr + %td + = f.label :key, '英文标识' + %small.smaller (必填) + %tr + %td{:style => 'position: relative;'} + = f.text_field :key, :class => :sl, :autofocus => true + %small.fade= t('tips.node_key_format') + %tr + %td + = f.label :name, '名称' + %small.smaller + 例如, Ruby on Rails + %small.smaller (必填) + %tr + %td + = f.text_field :name, :class => :sl + %tr + %td + = f.label :introduction, '一句话简介' + %tr + %td + = f.text_field :introduction, :class => :sll + %tr + %td + = f.label :custom_html, '自定义CSS' + %tr + %td + %small.fade <style type="text/css"> + = f.text_area :custom_css, :class => :mle + %small.fade <style> + %tr + %td + = f.label :custom_html, '自定义内容' + %small.smaller + 显示在右侧边栏的内容,支持html。 + %tr + %td + = f.text_area :custom_html, :class => :mle + %tr + %td + = f.check_box :quiet + = f.label :quiet, '禁止本节点话题出现在首页', :class => :small + %tr + %td + = f.submit '保存', :class => 'super normal button' diff --git a/app/views/admin/nodes/_move_form.js.haml b/app/views/admin/nodes/_move_form.js.haml new file mode 100644 index 0000000..eb100b6 --- /dev/null +++ b/app/views/admin/nodes/_move_form.js.haml @@ -0,0 +1,6 @@ += form_for [:admin, @node], :url => move_to_admin_node_path(@node), :remote => true do |f| + = f.label :plane_id, '移动到新位面' + = f.select :plane_id, options_from_collection_for_select(Plane.all - [@node.plane], :id, :name) + .sep5 + = f.submit '开始移动', :class => 'super normal button' + diff --git a/app/views/admin/nodes/_node.html.haml b/app/views/admin/nodes/_node.html.haml new file mode 100644 index 0000000..be0e9a2 --- /dev/null +++ b/app/views/admin/nodes/_node.html.haml @@ -0,0 +1,8 @@ +.cell.node{:id => node.html_id} + .fr + = admin_edit_button('修改节点', [node.plane, node], {:remote => true, :id => "edit_#{node.html_id}"}) + = link_to '移动', move_admin_node_path(node), :remote => true, :class => 'op admin_op' + = admin_delete_button(node, :remote => true) if node.can_delete? + = link_to node.name, go_path(node.key) + - if node.quiet + = image_tag 'ghost.png', :align => :top, :title => t('tips.quiet_node') diff --git a/app/views/admin/nodes/create.js.haml b/app/views/admin/nodes/create.js.haml new file mode 100644 index 0000000..59ca438 --- /dev/null +++ b/app/views/admin/nodes/create.js.haml @@ -0,0 +1,3 @@ +$("##{@plane.html_id}").append("#{escape_javascript render(@node) }") +$("##{@plane.html_id} .cell:first .fr").hide() +$.facebox.close() diff --git a/app/views/admin/nodes/destroy.js.haml b/app/views/admin/nodes/destroy.js.haml new file mode 100644 index 0000000..06c6d5e --- /dev/null +++ b/app/views/admin/nodes/destroy.js.haml @@ -0,0 +1,4 @@ +$("##{@node.html_id}").hide() +- if @node.plane.can_delete? + $("##{@node.plane.html_id}").replaceWith("#{escape_javascript render(@node.plane)}") + diff --git a/app/views/admin/nodes/move.js.haml b/app/views/admin/nodes/move.js.haml new file mode 100644 index 0000000..a3236af --- /dev/null +++ b/app/views/admin/nodes/move.js.haml @@ -0,0 +1 @@ +$.facebox("#{escape_javascript(render 'move_form')}") diff --git a/app/views/admin/nodes/move_to.js.haml b/app/views/admin/nodes/move_to.js.haml new file mode 100644 index 0000000..200b232 --- /dev/null +++ b/app/views/admin/nodes/move_to.js.haml @@ -0,0 +1 @@ +window.location.href = "#{admin_planes_path}" diff --git a/app/views/admin/nodes/show_form.js.haml b/app/views/admin/nodes/show_form.js.haml new file mode 100644 index 0000000..72e4139 --- /dev/null +++ b/app/views/admin/nodes/show_form.js.haml @@ -0,0 +1,2 @@ +$.facebox("#{escape_javascript(render('form', :plane => @plane, :node => @node))}"); +$("textarea").elastic() diff --git a/app/views/admin/nodes/update.js.haml b/app/views/admin/nodes/update.js.haml new file mode 100644 index 0000000..20df923 --- /dev/null +++ b/app/views/admin/nodes/update.js.haml @@ -0,0 +1,6 @@ +- target_id = '#' + @node.html_id +$.facebox.close() +$("#{target_id}").replaceWith("#{escape_javascript(render @node)}"); +setTimeout(function() { +$("#{target_id}").effect('highlight') +}, 500) diff --git a/app/views/admin/pages/_form.html.haml b/app/views/admin/pages/_form.html.haml new file mode 100644 index 0000000..6167905 --- /dev/null +++ b/app/views/admin/pages/_form.html.haml @@ -0,0 +1,46 @@ += form_for [:admin, page] do |f| + %table + %tr + %td + = f.label :title, '页面标题' + %small.smaller (必填) + %tr + %td + = f.text_field :title, :class => :sl + .sep10 + %tr + %td + = f.label :key, '英文标题' + %small.smaller (必填) + %tr + %td + = f.text_field :key, :class => :sls + %small.smaller 只允许英文字符和下划线 + .sep10 + %tr + %td + = f.label :content, '页面内容' + %small.smaller (必填) + .sep5 + %tr + %td + = render 'shared/preview_widget', :ref => :page_content, :type => :page + = f.text_area :content, :class => :mle + - if Siteconf.allow_markdown_in_pages? + .sep3 + %span.fade 支持 Markdown 格式 + .sep5 + %tr + %td + = f.label :published, '发布状态' + %tr + %td + = f.radio_button 'published', true + %span.published 立刻发布 + = f.radio_button 'published', false + %span.draft 保存为草稿 + .sep10 + %tr + %td + = f.submit '保存', :class => 'super normal button' + diff --git a/app/views/admin/pages/_page.html.haml b/app/views/admin/pages/_page.html.haml new file mode 100644 index 0000000..cdea184 --- /dev/null +++ b/app/views/admin/pages/_page.html.haml @@ -0,0 +1,11 @@ +%tr{:id => page.html_id} + %td{:align => :right, :width => 100} + = link_to page_real_url(page), page_real_url(page) + %td{:align => :center, :width => :auto} + = page.title + %td{:align => :center, :width => :auto} + = page_publish_status(page) + %td{:align => :left} + = admin_edit_button('修改页面', page) + = admin_delete_button(page) + diff --git a/app/views/admin/pages/action.html.haml b/app/views/admin/pages/action.html.haml new file mode 100644 index 0000000..0bbabde --- /dev/null +++ b/app/views/admin/pages/action.html.haml @@ -0,0 +1,5 @@ +.box + .cell + = build_admin_navigation([link_to('页面管理', admin_pages_path), @title]) + .inner + = render 'form', :page => @page diff --git a/app/views/admin/pages/index.html.haml b/app/views/admin/pages/index.html.haml new file mode 100644 index 0000000..828ca79 --- /dev/null +++ b/app/views/admin/pages/index.html.haml @@ -0,0 +1,27 @@ +- content_for :template_js do + :plain + rabel.sortable("table tbody", "#{sort_admin_pages_path}", { + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + } + }) + +.box + .cell + .fr + = admin_create_button('创建新页面', :page) + = build_admin_navigation([@title]) + .inner + %table{:cellpadding => 5, :cellspacing => 0, :border => 0, :width => '100%'} + %thead + %tr + %th{:align => :right, :width => 100} URL + %th{:align => :center, :width => :auto} 标题 + %th{:align => :center, :width => :auto} 发布状态 + %th{:align => :left} 操作 + %tbody + = render @pages + diff --git a/app/views/admin/planes/_form.html.haml b/app/views/admin/planes/_form.html.haml new file mode 100644 index 0000000..4c48a92 --- /dev/null +++ b/app/views/admin/planes/_form.html.haml @@ -0,0 +1,13 @@ += form_for [:admin, plane], :remote => true do |f| + %table + %tr + %td + = f.label :name, '名称' + %small.smaller + (必填) + %tr + %td + = f.text_field :name, :class => :sl, :autofocus => true + %tr + %td + = f.submit '保存', :class => 'super normal button' diff --git a/app/views/admin/planes/_plane.html.haml b/app/views/admin/planes/_plane.html.haml new file mode 100644 index 0000000..88a4059 --- /dev/null +++ b/app/views/admin/planes/_plane.html.haml @@ -0,0 +1,11 @@ +.box.plane{:id => plane.html_id} + .cell + - if plane.can_delete? + .fr + = admin_delete_button(plane, :remote => true) + = plane.name + %span{:style => 'margin-left: 5px;'} + = admin_edit_button('修改位面', plane, :remote => true) + = admin_create_button('添加节点', [plane, :node], {:class => 'op admin_op', :remote => true}) + = render plane.nodes.default_order +.sep10 diff --git a/app/views/admin/planes/_sort_plane.html.haml b/app/views/admin/planes/_sort_plane.html.haml new file mode 100644 index 0000000..634d3b6 --- /dev/null +++ b/app/views/admin/planes/_sort_plane.html.haml @@ -0,0 +1 @@ +.sort_item{:id => plane.html_id}= plane.name diff --git a/app/views/admin/planes/_sort_planes.html.haml b/app/views/admin/planes/_sort_planes.html.haml new file mode 100644 index 0000000..ba07721 --- /dev/null +++ b/app/views/admin/planes/_sort_planes.html.haml @@ -0,0 +1,4 @@ +%strong 位面拖动排序 += render :partial => 'sort_plane', :collection => planes, :as => :plane, :formats => :html +.sort_actions + = link_to '完成排序', 'javascript:window.location.reload()', :class => 'super normal button' diff --git a/app/views/admin/planes/create.js.haml b/app/views/admin/planes/create.js.haml new file mode 100644 index 0000000..49af127 --- /dev/null +++ b/app/views/admin/planes/create.js.haml @@ -0,0 +1,2 @@ +$("#planes").append("#{escape_javascript render(@plane)}") +$.facebox.close() diff --git a/app/views/admin/planes/destroy.js.haml b/app/views/admin/planes/destroy.js.haml new file mode 100644 index 0000000..45cbf26 --- /dev/null +++ b/app/views/admin/planes/destroy.js.haml @@ -0,0 +1 @@ +$("##{@plane.html_id}").hide() diff --git a/app/views/admin/planes/index.html.haml b/app/views/admin/planes/index.html.haml new file mode 100644 index 0000000..2520c37 --- /dev/null +++ b/app/views/admin/planes/index.html.haml @@ -0,0 +1,14 @@ +- content_for :template_js do + :plain + rabel.sortable(".plane", "#{sort_admin_nodes_path}") + +.box + .inner + .fr + = admin_create_button('添加位面', :plane, :remote => true) + = link_to '位面排序', sort_admin_planes_path, :class => 'super normal button', :remote => true + = build_admin_navigation([@title]) +.sep20 + +#planes + = render @planes diff --git a/app/views/admin/planes/show_form.js.haml b/app/views/admin/planes/show_form.js.haml new file mode 100644 index 0000000..7d29641 --- /dev/null +++ b/app/views/admin/planes/show_form.js.haml @@ -0,0 +1 @@ +$.facebox("#{escape_javascript(render('form', :plane => @plane))}"); diff --git a/app/views/admin/planes/sort.js.haml b/app/views/admin/planes/sort.js.haml new file mode 100644 index 0000000..21eed78 --- /dev/null +++ b/app/views/admin/planes/sort.js.haml @@ -0,0 +1,2 @@ +$.facebox("#{escape_javascript render('sort_planes', :planes => @planes, :formats => :html)}"); +rabel.sortable("#facebox .content", "#{sort_admin_planes_path}") diff --git a/app/views/admin/rewards/_form.html.haml b/app/views/admin/rewards/_form.html.haml new file mode 100644 index 0000000..5dd8443 --- /dev/null +++ b/app/views/admin/rewards/_form.html.haml @@ -0,0 +1,26 @@ += form_for [:admin, @user, @reward], :remote => true do |f| + %h3 + - if @reward_type == Reward::TYPE_GRANT + 奖励给 + = @user.nickname + - else + 从 + = @user.nickname + 帐户中扣除 + + = f.text_field :amount_str, :class => 'sls quantity', :autofocus => true, :placeholder => '必填' + = Siteconf.reward_title + - if @reward_type == Reward::TYPE_REVOKE + %small + (可用余额: + %span.red= @user.reward + ) + .sep3 + = f.label :reason, '理由' + = f.text_area :reason, :class => :ml, :placeholder => '必填' + .sep3 + = f.hidden_field :reward_type + - if @reward_type == 'grant' + = f.submit '发放奖励', :class => 'super normal button' + - else + = f.submit "扣除#{Siteconf.reward_title}", :class => 'super normal button' diff --git a/app/views/admin/rewards/_reward.html.haml b/app/views/admin/rewards/_reward.html.haml new file mode 100644 index 0000000..e0bc3ba --- /dev/null +++ b/app/views/admin/rewards/_reward.html.haml @@ -0,0 +1,16 @@ +%tr.highlight + %td.d + %small.gray= l reward.created_at, :format => :long + %td.d + %strong= user_profile_link(reward.user) + %td.d + - if reward.amount > 0 + %strong.green.quantity= reward.amount + - else + %strong.red.quantity= reward.amount + %td.d + = reward.balance + %td.d + = user_profile_link(reward.admin_user) + %td.d.last + .gray= reward.reason diff --git a/app/views/admin/rewards/create.js.haml b/app/views/admin/rewards/create.js.haml new file mode 100644 index 0000000..3497d3a --- /dev/null +++ b/app/views/admin/rewards/create.js.haml @@ -0,0 +1,8 @@ +:plain + var balance = $("#reward_balance") + if (balance.length > 0) { + balance.html("#{@user.reward}"); + $.facebox.close(); + } else { + window.location.reload(); + } diff --git a/app/views/admin/rewards/index.html.haml b/app/views/admin/rewards/index.html.haml new file mode 100644 index 0000000..97d1796 --- /dev/null +++ b/app/views/admin/rewards/index.html.haml @@ -0,0 +1,16 @@ +.box + .cell + = build_admin_navigation([@title]) + %table.data{:cellpadding => 5, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.h{:width => 110} 时间 + %td.h{:width => 80} 帐户 + %td.h{:width => 60} 数额 + %td.h{:width => 60} 当前余额 + %td.h{:width => 80} 管理员 + %td.h.last{:width => :auto} 理由 + + = render @rewards + .inner + = paginate @rewards + diff --git a/app/views/admin/rewards/new.js.haml b/app/views/admin/rewards/new.js.haml new file mode 100644 index 0000000..c6a072b --- /dev/null +++ b/app/views/admin/rewards/new.js.haml @@ -0,0 +1 @@ +$.facebox('#{escape_javascript render('form')}'); diff --git a/app/views/admin/site_settings/appearance.html.haml b/app/views/admin/site_settings/appearance.html.haml new file mode 100644 index 0000000..7d941cf --- /dev/null +++ b/app/views/admin/site_settings/appearance.html.haml @@ -0,0 +1,67 @@ +.box + .cell + = build_admin_navigation([@title]) + .inner + = form_for :site_settings, :url => admin_site_settings_path, :method => :put do |f| + %table + %tr + %td + %td + = f.submit '保存', :class => 'super normal button' + %tr + %td + = label_tag 'settings[theme]', '设计风格' + %td + = select_tag 'settings[theme]', options_for_select(Settings.themes.invert.to_a, @settings.theme) + .sep10 + %tr + %td + = label_tag 'settings[nav_position]', '节点导航位置' + %td + = select_tag 'settings[nav_position]', options_for_select([['头部', 'top'], ['侧边栏', 'sidebar'], ['底部', 'bottom']], @settings.nav_position) + .sep10 + + %tr + %td + = label_tag 'settings[topic_list_style]', '帖子列表风格' + %td + = select_tag 'settings[topic_list_style]', options_for_select(Settings.topic_list_styles.invert.to_a, @settings.topic_list_style) + .sep10 + %tr + %td + = label_tag 'settings[custom_logo]', '自定义Logo' + %td + = text_field_tag 'settings[custom_logo]', @settings.custom_logo, :class => :sl, :placeholder => t('tips.cloud_file_url') + %small.smaller 推荐尺寸: 100 x 40 + .sep10 + %tr + %td + = label_tag 'settings[global_banner]', '自定义Banner' + %td + = text_field_tag 'settings[global_banner]', @settings.global_banner, :class => :sl, :placeholder => t('tips.cloud_file_url') + %small.smaller 推荐尺寸: 960 x 145 + .sep10 + %tr + %td + = label_tag 'settings[custom_css]', '自定义CSS' + %td + %span.fade <style type="text/css"> + .sep3 + = text_area_tag 'settings[custom_css]', @settings.custom_css, :class => :ml + .sep3 + %span.fade </style> + .sep10 + %tr + %td + = label_tag 'settings[custom_js]', '自定义JavaScript' + %td + %span.fade <script type="text/javascript"> + .sep3 + = text_area_tag 'settings[custom_js]', @settings.custom_js, :class => :ml + .sep3 + %span.fade </script> + .sep10 + %tr + %td + %td + = f.submit '保存', :class => 'super normal button' diff --git a/app/views/admin/site_settings/show.html.haml b/app/views/admin/site_settings/show.html.haml new file mode 100644 index 0000000..20632fb --- /dev/null +++ b/app/views/admin/site_settings/show.html.haml @@ -0,0 +1,201 @@ +.box + .cell + = build_admin_navigation([@title]) + .inner + = form_for :site_settings, :url => admin_site_settings_path, :method => :put do |f| + %table + %tr + %td + = f.submit '保存', :class => 'super normal button' + %tr + %td + = label_tag 'settings[site_name]', '网站名称' + %small.smaller (必填) + %td + = text_field_tag 'settings[site_name]', @settings.site_name, :class => :sls + .sep10 + + %tr + %td + = label_tag 'settings[site_host]', '域名' + %small.smaller (必填) + %td + = text_field_tag 'settings[site_host]', @settings.site_host, :class => :sl + %small.fade 不需要加 http:// + .sep10 + + %tr + %td + = label_tag 'settings[welcome_tip]', '欢迎信息' + %small.smaller (必填) + %td + = text_field_tag 'settings[welcome_tip]', @settings.welcome_tip, :class => :sl + %small.smaller 支持HTML + .sep10 + %tr + %td + = label_tag 'settings[short_intro]', '简短介绍' + %td + = text_field_tag 'settings[short_intro]', @settings.short_intro, :class => :sl + %small.smaller 网站简短介绍, 显示在右侧边栏 + .sep10 + %tr + %td + = label_tag 'settings[marketing_str]', '市场宣传关键字' + %td + = text_field_tag 'settings[marketing_str]', @settings.marketing_str, :class => :sl + %small.smaller 用英文逗号(,)隔开 + .sep10 + %tr + %td + = label_tag 'settings[ga_id]', 'Google Analytics ID' + %td + = text_field_tag 'settings[ga_id]', @settings.ga_id, :class => :sls + %small.smaller 例如: UA-12345678-01 + .sep10 + + %tr + %td + = label_tag 'settings[default_search_engine]', '站内搜索引擎' + %td + = select_tag 'settings[default_search_engine]', options_for_select([['谷歌', 'google'], ['百度', 'baidu'], ['必应', 'bing']], @settings.default_search_engine) + .sep10 + %tr + %td + = label_tag 'settings[show_captcha]', '注册验证码' + %td + = radio_button_tag 'settings[show_captcha]', 'on', @settings.show_captcha? + 开启 + = radio_button_tag 'settings[show_captcha]', 'off', !@settings.show_captcha? + 关闭 + %tr + %td + = label_tag 'settings[show_community_stats]', '社区运行状态' + %td + = radio_button_tag 'settings[show_community_stats]', 'on', @settings.show_community_stats? + 显示 + = radio_button_tag 'settings[show_community_stats]', 'off', !@settings.show_community_stats? + 隐藏 + %tr + %td + = label_tag 'settings[allow_markdown_in_topics]', '话题允许 Markdown' + %td + = radio_button_tag 'settings[allow_markdown_in_topics]', 'on', @settings.allow_markdown_in_topics? + 允许 + = radio_button_tag 'settings[allow_markdown_in_topics]', 'off', !@settings.allow_markdown_in_topics? + 关闭 + %tr + %td + = label_tag 'settings[allow_markdown_in_comments]', '回复允许 Markdown' + %td + = radio_button_tag 'settings[allow_markdown_in_comments]', 'on', @settings.allow_markdown_in_comments? + 允许 + = radio_button_tag 'settings[allow_markdown_in_comments]', 'off', !@settings.allow_markdown_in_comments? + 关闭 + %tr + %td + = label_tag 'settings[allow_markdown_in_pages]', '页面允许 Markdown' + %td + = radio_button_tag 'settings[allow_markdown_in_pages]', 'on', @settings.allow_markdown_in_pages? + 允许 + = radio_button_tag 'settings[allow_markdown_in_pages]', 'off', !@settings.allow_markdown_in_pages? + 关闭 + %tr + %td + = label_tag 'settings[custom_head_tags]', '自定义Head标签' + %td + = text_area_tag 'settings[custom_head_tags]', @settings.custom_head_tags, :class => :ml + %small.smaller 可以添加<meta>, <script>, <style>等头部标签 + .sep10 + + %tr + %td + = label_tag 'settings[seo_keywords_str]', 'SEO 关键字' + %td + = text_area_tag 'settings[seo_keywords_str]', @settings.seo_keywords_str, :class => :ml + %small.smaller 用英文逗号(,)隔开 + .sep10 + + %tr + %td + = label_tag 'settings[seo_description]', 'SEO 描述' + %td + = text_area_tag 'settings[seo_description]', @settings.seo_description, :class => :ml + %small.smaller 用于HTML meta标签 + .sep10 + %tr + %td + = label_tag 'settings[splash]', 'Hero Unit' + %td + = text_area_tag 'settings[splash]', @settings.splash, :class => :ml + %small.smaller 支持HTML + .sep10 + %tr + %td + = label_tag 'settings[global_sidebar_block]', '全局侧边栏' + %td + = text_area_tag 'settings[global_sidebar_block]', @settings.global_sidebar_block, :class => :ml + %small.smaller 支持HTML + .sep10 + %tr + %td + = label_tag 'settings[footer]', '页面底部 [普通版]' + %td + = text_area_tag 'settings[footer]', @settings.footer, :class => :ml + %small.smaller 支持HTML + .sep10 + %tr + %td + = label_tag 'settings[mobile_footer]', '页面底部 [移动版]' + %td + = text_area_tag 'settings[mobile_footer]', @settings.mobile_footer, :class => :sl + %small.smaller 支持HTML + .sep10 + %tr + %td + = label_tag 'settings[sticky_topics_heading]', '置顶话题提示' + %td + = text_field_tag 'settings[sticky_topics_heading]', @settings.sticky_topics_heading, :class => :sl + .sep10 + + %tr + %td + = label_tag 'settings[latest_topics_heading]', '最新话题提示' + %td + = text_field_tag 'settings[latest_topics_heading]', @settings.latest_topics_heading, :class => :sl + .sep10 + + %tr + %td + = label_tag 'settings[reward_title]', '奖励名称' + %td + = text_field_tag 'settings[reward_title]', @settings.reward_title, :class => :sls + %small.smaller 例如: 银币,金币,积分,优惠券,代金券,蓝钻, Q币等 + .sep10 + %tr + %td + = label_tag 'settings[topic_editable_period_str]', '话题编辑限制' + %td + = text_field_tag 'settings[topic_editable_period_str]', @settings.topic_editable_period_str, :class => 'sls quantity' + 分钟 + %small.smaller 允许用户修改话题的时间 + .sep10 + %tr + %td + = label_tag 'settings[pagination_topics]', '节点话题' + %td + = text_field_tag 'settings[pagination_topics]', @settings.pagination_topics, :class => 'sls quantity' + \/页 + .sep10 + + %tr + %td + = label_tag 'settings[pagination_comments]', '回复' + %td + = text_field_tag 'settings[pagination_comments]', @settings.pagination_comments, :class => 'sls quantity' + \/页 + .sep10 + %tr + %td + %td + = f.submit '保存', :class => 'super normal button' diff --git a/app/views/admin/topics/_table_view.html.haml b/app/views/admin/topics/_table_view.html.haml new file mode 100644 index 0000000..c5367f2 --- /dev/null +++ b/app/views/admin/topics/_table_view.html.haml @@ -0,0 +1,32 @@ +%table.topics + %thead + %tr + %th.w50 ID + %th.auto{:align => :left} 节点 + %th.auto{:align => :left} 标题 + %th.auto{:align => :left} 作者 + %th.auto{:align => :right} 回复数 + %th.auto{:align => :right} 浏览量 + %th.auto{:align => :right} 创建时间 + %th.w100 操作 + %tbody + - topics.each do |topic| + %tr.highlight + %td.w50 + %strong.green + = topic.id + %td.auto + = link_to topic.node.name, go_path(topic.node.key) + %td.auto + = link_to topic.title, t_path(topic.id) + %td.auto + = user_profile_link(topic.user) + %td.auto{:align => :right} + = topic.comments_count + %td.auto{:align => :right} + = topic.hit + %td.auto{:align => :right} + %small.fade= time_ago_in_words(topic.created_at) + %td.w100 + = link_to '编辑', edit_node_topic_path(topic.node, topic), :class => :op + = link_to '删除', admin_topic_path(topic), :class => 'op op_danger', :method => :delete, :data => {:confirm => t(:delete_confirm)} diff --git a/app/views/admin/topics/index.html.haml b/app/views/admin/topics/index.html.haml new file mode 100644 index 0000000..9cd8a16 --- /dev/null +++ b/app/views/admin/topics/index.html.haml @@ -0,0 +1,8 @@ +.box + .cell + = build_admin_navigation([@title]) + .cell + = render 'table_view', :topics => @topics + .inner{:align => :center} + = paginate @topics + diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml new file mode 100644 index 0000000..bed1cb2 --- /dev/null +++ b/app/views/admin/users/_user.html.haml @@ -0,0 +1,37 @@ +%tr.highlight{:id => user.html_id} + %td{:align => :right}= user.id + %td.auto{:align => :left} + - if user.blocked? + %del.red= user.nickname + - else + %strong + = user_profile_link(user, :class => :black) + %td.w50{:align => :left} + - if user.root? + %strong.green ROOT + - elsif user.admin? + = image_tag 'star.png' + - else + %span.fade 普通 + - if user.blocked + .sep3 + %small.red 已屏蔽 + + %td.auto{:align => :left}= user.email + %td.auto{:align => :right} + = user.reward + %td.center + - if can? :edit_info, user + = link_to '修改用户信息', edit_admin_user_path(user), :class => :op + + - if current_user.can_manage_site? and user != current_user and (not user.root?) + - if user.admin? + = link_to '取消管理权限', toggle_admin_admin_user_path(user), :method => :put, :remote => true, :class => :opo + - else + = link_to '提升为管理员', toggle_admin_admin_user_path(user), :method => :put, :remote => true, :class => :op + + - if user.blocked? + = link_to '取消屏蔽', toggle_blocked_admin_user_path(user), :method => :put, :remote => true, :class => 'op op_danger' + - else + = link_to '屏蔽', toggle_blocked_admin_user_path(user), :method => :put, :remote => true, :class => 'op op_danger' + diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml new file mode 100644 index 0000000..8be7f77 --- /dev/null +++ b/app/views/admin/users/edit.html.haml @@ -0,0 +1,38 @@ +.box + .cell + = build_admin_navigation([@title]) + .cell + < + = link_to '返回用户管理', admin_users_path + "?nickname=#{url_encode @user.nickname}" + + = form_for [:admin, @user] do |f| + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 120, :align => :right} 用户名 + %td{:width => 200, :align => :left} + = f.text_field :nickname, :class => :sls + %tr + %td{:width => 120, :align => :right} 电子邮件 + %td{:width => 200, :align => :left} + = f.email_field :email, :class => :sl + %tr + %td{:width => 120, :align => :right} 新密码 + %td{:width => 200, :align => :left} + = f.password_field :password, :class => :sl + %tr + %td{:width => 120, :align => :right} 确认新密码 + %td{:width => 200, :align => :left} + = f.password_field :password_confirmation, :class => :sl + + = render 'users/account_detail_form', :f => f + + %tr + %td{:width => 120, :align => :right}= Siteconf.reward_title + %td{:width => 200, :align => :left} + = f.text_field :reward, :class => :sls + + %tr + %td{:align => :right} + %td{:width => 200, :align => :left} + = f.submit '更新用户信息', :class => 'super normal button' + diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml new file mode 100644 index 0000000..0c6952b --- /dev/null +++ b/app/views/admin/users/index.html.haml @@ -0,0 +1,23 @@ +.box + .cell + .fr + = form_for '', :url => admin_users_path, :method => :get do + = label_tag :nickname, '用户昵称', :class => :small + = text_field_tag :nickname, params[:nickname], :class => :sls + = submit_tag '搜索', :class => 'super normal button' + + = build_admin_navigation([@title]) + .cell + %table{:cellpadding => 5, :cellspacing => 0, :border => 0, :width => '100%'} + %thead + %tr + %th{:align => :right} ID + %th.w50{:align => :left} 昵称 + %th.auto{:align => :left} 角色 + %th.auto{:align => :left} Email + %th.auto{:align => :right}= Siteconf.reward_title + %th 操作 + %tbody + = render @users + .inner{:align => :center} + = paginate @users diff --git a/app/views/admin/users/toggle_admin.js.haml b/app/views/admin/users/toggle_admin.js.haml new file mode 100644 index 0000000..029f4ab --- /dev/null +++ b/app/views/admin/users/toggle_admin.js.haml @@ -0,0 +1,2 @@ +$("##{@user.html_id}").replaceWith("#{escape_javascript(render('user', :user => @user))}"); +$("##{@user.html_id}").effect('highlight'); diff --git a/app/views/admin/users/toggle_blocked.js.haml b/app/views/admin/users/toggle_blocked.js.haml new file mode 100644 index 0000000..029f4ab --- /dev/null +++ b/app/views/admin/users/toggle_blocked.js.haml @@ -0,0 +1,2 @@ +$("##{@user.html_id}").replaceWith("#{escape_javascript(render('user', :user => @user))}"); +$("##{@user.html_id}").effect('highlight'); diff --git a/app/views/admin/welcome_admin/index.html.haml b/app/views/admin/welcome_admin/index.html.haml new file mode 100644 index 0000000..e84ba6d --- /dev/null +++ b/app/views/admin/welcome_admin/index.html.haml @@ -0,0 +1,55 @@ +.box{:style => 'float: right; width: 330px;'} + .cell + 最新用户 + .inner + %table.topics + %thead + %tr + %th.w50 ID + %th.auto{:align => :left} 用户名 + %th.w100 注册时间 + %tbody + - User.order('created_at DESC').limit(5).each do |user| + %tr + %td.w50 + %strong.green + = user.id + %td.auto + = user_profile_link(user) + %td.w100 + %small.fade= time_ago_in_words(user.created_at) + +.box{:style => 'width: 390px;'} + .cell + 社区运行状态 + .inner + %table{:cellpadding => 3, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:align => :right, :width => '40%'} + %span.fade 注册会员总数 + %td{:align => :left} + %strong= User.cached_count + %tr + %td{:align => :right, :width => '40%'} + %span.fade 主题总数 + %td{:align => :left} + %strong= Topic.cached_count + %tr + %td{:align => :right, :width => '40%'} + %span.fade 回复总数 + %td{:align => :left} + %strong= Comment.cached_count + +.sep20 +.box{:style => 'width: 390px;'} + .cell + 系统清理 + .inner + %table{:cellpadding => 3, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:align => :right, :width => '40%'} + %span.fade 可清理提醒 + %td{:align => :left} + %strong= @notifications_to_clear + %td{:align => :left} + = link_to '删除已读提醒', clear_admin_notifications_path, :method => :delete, :class => 'op admin_op op_danger' if @notifications_to_clear > 0 diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml new file mode 100644 index 0000000..52a21cf --- /dev/null +++ b/app/views/comments/_comment.html.haml @@ -0,0 +1,31 @@ +- comment_user = comment.cached_assoc_object(:user) +%article + .cell.reply.hoverable{:id => comment.html_id, :class => comment_user.can_manage_site? ? 'admin' : ''} + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:valign => :top, :width => 48} + = user_profile_avatar_link(comment_user, :medium) + %td{:width => 10} + %td{:width => :auto, :valign => :top} + .fr + - if user_signed_in? and current_user.can_manage_site? + %small.hover_action + = link_to 'EDIT', edit_comment_path(comment), :class => :edit, :remote => :true + = link_to 'DEL', comment_path(comment), :method => :delete, :data => {:confirm => I18n.t(:delete_confirm)}, :class => :op_danger, :remote => true + %strong + %small.snow + - _num = comment_counter + 1 + - if @total_pages > 1 + = "##{Siteconf.pagination_comments.to_i * (@current_page.to_i - 1) + _num} -" + - else + = "##{_num} -" + = time_ago_in_words(comment.created_at) + - if user_signed_in? + = image_tag 'reply_button.png', :align => :absmiddle, :border => 0, :class => 'clickable mention_button', :data => {:mention => "@#{comment_user.nickname}"} + .sep3 + %strong + = user_profile_link(comment_user, :class => :dark) + = show_posting_device(comment) + .sep5 + .content.reply_content= format_comment comment.content + diff --git a/app/views/comments/_comment.mobile.haml b/app/views/comments/_comment.mobile.haml new file mode 100644 index 0000000..7634c15 --- /dev/null +++ b/app/views/comments/_comment.mobile.haml @@ -0,0 +1,19 @@ +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.avatar_mini{:valign => :top} + = mini_avatar(comment.user) + %td{:style => 'padding-left: 5px;', :valign => :top} + .fr + %span.ago + - _num = comment_counter + 1 + - if @total_pages > 1 + = "##{Siteconf.pagination_comments.to_i * (@page_num.to_i - 1) + _num} -" + - else + = "##{_num} -" + = time_ago_in_words(comment.created_at) + = user_profile_link(comment.user) + = show_posting_device(comment) + .sep5 + = format_comment comment.content + diff --git a/app/views/comments/_form.js.haml b/app/views/comments/_form.js.haml new file mode 100644 index 0000000..656aaaa --- /dev/null +++ b/app/views/comments/_form.js.haml @@ -0,0 +1,5 @@ +%h3.fade 修改回复 += form_for @comment, :remote => true do |f| + = f.text_area :content, :class => :mll, :autofocus => true + .sep5 + = f.submit '保存', :class => 'super normal button' diff --git a/app/views/comments/destroy.js.haml b/app/views/comments/destroy.js.haml new file mode 100644 index 0000000..b5dc437 --- /dev/null +++ b/app/views/comments/destroy.js.haml @@ -0,0 +1,2 @@ +$("##{@comment.html_id}").effect('highlight'); +$("##{@comment.html_id}").slideUp(); diff --git a/app/views/comments/edit.js.haml b/app/views/comments/edit.js.haml new file mode 100644 index 0000000..7b9debe --- /dev/null +++ b/app/views/comments/edit.js.haml @@ -0,0 +1,2 @@ +$.facebox("#{escape_javascript(render('form'))}"); +$("textarea").elastic() diff --git a/app/views/comments/update.js.haml b/app/views/comments/update.js.haml new file mode 100644 index 0000000..e464a67 --- /dev/null +++ b/app/views/comments/update.js.haml @@ -0,0 +1,2 @@ +$("##{@comment.html_id}").find('.reply_content').html('#{escape_javascript(parse_markdown(@comment.content))}'); +$.facebox.close(); diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..b7ae403 --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,12 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :email %>
+ <%= f.email_field :email %>
+ +
<%= f.submit "Resend confirmation instructions" %>
+<% end %> + +<%= render :partial => "devise/shared/links" %> \ No newline at end of file diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..a6ea8ca --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @resource.email %>!

+ +

You can confirm your account through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %>

diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml new file mode 100644 index 0000000..36df096 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.haml @@ -0,0 +1,7 @@ +- reset_url = edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) +Hi #{@resource.nickname}: +%br/ +%p 请点击下面的链接重新设置你的密码: +%p=link_to reset_url, reset_url +%p= Siteconf.site_name +%p 如果本次密码重设请求不是由你发起,你可以安全地忽略本邮件。 diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..2263c21 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive amount of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %>

diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml new file mode 100644 index 0000000..cfd1cc1 --- /dev/null +++ b/app/views/devise/passwords/edit.html.haml @@ -0,0 +1,24 @@ +- @title = '重新设置密码' +.box + .cell + = build_navigation([@title]) + .inner + = form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| + = devise_error_messages! + = f.hidden_field :reset_password_token + %table.form + %tr + %td.left + = f.label :password, '新密码' + %td.right + = f.password_field :password, :class => :sl + %tr + %td.left + = f.label :password_confirmation, '请再输入一次' + %td.right + = f.password_field :password_confirmation, :class => :sl + %tr + %td.left + %td.right + = f.submit '继续', :class => 'super normal button' + diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml new file mode 100644 index 0000000..73fbacb --- /dev/null +++ b/app/views/devise/passwords/new.html.haml @@ -0,0 +1,27 @@ +- @title = '重新设置密码' +.box + .cell + = build_navigation([@title]) + .inner + = form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| + = devise_error_messages! + %table.form + %tr + %td.left + = f.label :nickname, '用户名' + %td.right + = f.text_field :nickname, :class => :sl + %tr + %td.left + = f.label :email, '注册邮箱' + %td.right + = f.email_field :email, :class => :sl + %tr + %td.left + %td.right + = f.submit @title, :class => 'super normal button' + %tr + %td.left + %td.right + %span.fade 24 小时内,至多可以重新设置密码 2 次。 + diff --git a/app/views/devise/registrations/_form.html.haml b/app/views/devise/registrations/_form.html.haml new file mode 100644 index 0000000..b630458 --- /dev/null +++ b/app/views/devise/registrations/_form.html.haml @@ -0,0 +1,33 @@ += form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| + - f.object.captcha = '' + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 80, :align => :right} 用户名 + %td{:width => 200, :align => :left} + = f.text_field :nickname, :class => :sl, :autofocus => true + %tr + %td{:width => 80, :align => :right} 电子邮件 + %td{:width => 200, :align => :left} + = f.email_field :email, :class => :sl + %tr + %td{:width => 80, :align => :right} 密码 + %td{:width => 200, :align => :left} + = f.password_field :password, :class => :sl + %tr + %td{:width => 80, :align => :right} 密码确认 + %td{:width => 200, :align => :left} + = f.password_field :password_confirmation, :class => :sl + + - if Siteconf.show_captcha? + %tr + %td{:width => 80, :align => :right} 验证码 + %td{:width => 200, :align => :left} + = image_tag captcha_path(:format => :gif), :class => :captcha + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + = f.text_field :captcha, :class => 'sls captcha' + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + = f.submit '注册', :class => 'super normal button' diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..90b73bd --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,25 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :email %>
+ <%= f.email_field :email %>
+ +
<%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password %>
+ +
<%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation %>
+ +
<%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password %>
+ +
<%= f.submit "Update" %>
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :data => {:confirm => "Are you sure?"}, :method => :delete %>.

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml new file mode 100644 index 0000000..aaab436 --- /dev/null +++ b/app/views/devise/registrations/new.html.haml @@ -0,0 +1,5 @@ +.box + .cell + = build_navigation(['注册']) + .inner + = render 'devise/registrations/form' diff --git a/app/views/devise/registrations/new.mobile.haml b/app/views/devise/registrations/new.mobile.haml new file mode 100644 index 0000000..623b508 --- /dev/null +++ b/app/views/devise/registrations/new.mobile.haml @@ -0,0 +1,3 @@ +- add_breadcrumb '新用户注册' +.cell + = render :partial => 'devise/registrations/form', :formats => :html diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb new file mode 100644 index 0000000..f2c4131 --- /dev/null +++ b/app/views/devise/shared/_links.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Sign in", new_session_path(resource_name) %>
+<% end -%> + +<%# if devise_mapping.registerable? && controller_name != 'registrations' %> + <%# link_to "注册", new_registration_path(resource_name) %> +<%# end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> + <%= link_to "我忘记密码了", new_password_path(resource_name) %> +<% end -%> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
+ <% end -%> +<% end -%> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..c6cdcfe --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,12 @@ +

Resend unlock instructions

+ +<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :email %>
+ <%= f.email_field :email %>
+ +
<%= f.submit "Resend unlock instructions" %>
+<% end %> + +<%= render :partial => "devise/shared/links" %> \ No newline at end of file diff --git a/app/views/kaminari/_first_page.html.haml b/app/views/kaminari/_first_page.html.haml new file mode 100644 index 0000000..fee8112 --- /dev/null +++ b/app/views/kaminari/_first_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "First" page +-# available local variables +-# url: url to the first page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.first + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/_first_page.mobile.haml b/app/views/kaminari/_first_page.mobile.haml new file mode 100644 index 0000000..fee8112 --- /dev/null +++ b/app/views/kaminari/_first_page.mobile.haml @@ -0,0 +1,9 @@ +-# Link to the "First" page +-# available local variables +-# url: url to the first page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.first + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/_gap.html.haml b/app/views/kaminari/_gap.html.haml new file mode 100644 index 0000000..f82f185 --- /dev/null +++ b/app/views/kaminari/_gap.html.haml @@ -0,0 +1,8 @@ +-# Non-link tag that stands for skipped pages... +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.page.gap + = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/_gap.mobile.haml b/app/views/kaminari/_gap.mobile.haml new file mode 100644 index 0000000..f82f185 --- /dev/null +++ b/app/views/kaminari/_gap.mobile.haml @@ -0,0 +1,8 @@ +-# Non-link tag that stands for skipped pages... +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.page.gap + = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/_last_page.html.haml b/app/views/kaminari/_last_page.html.haml new file mode 100644 index 0000000..6e41d23 --- /dev/null +++ b/app/views/kaminari/_last_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Last" page +-# available local variables +-# url: url to the last page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.last + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/_last_page.mobile.haml b/app/views/kaminari/_last_page.mobile.haml new file mode 100644 index 0000000..6e41d23 --- /dev/null +++ b/app/views/kaminari/_last_page.mobile.haml @@ -0,0 +1,9 @@ +-# Link to the "Last" page +-# available local variables +-# url: url to the last page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.last + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 0000000..e87ab4e --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.next + = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote diff --git a/app/views/kaminari/_next_page.mobile.haml b/app/views/kaminari/_next_page.mobile.haml new file mode 100644 index 0000000..e87ab4e --- /dev/null +++ b/app/views/kaminari/_next_page.mobile.haml @@ -0,0 +1,9 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.next + = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote diff --git a/app/views/kaminari/_page.html.haml b/app/views/kaminari/_page.html.haml new file mode 100644 index 0000000..55c85ea --- /dev/null +++ b/app/views/kaminari/_page.html.haml @@ -0,0 +1,10 @@ +-# Link showing page number +-# available local variables +-# page: a page object for "this" page +-# url: url to this page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span{:class => "k_page#{' current' if page.current?}"} + = link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/_page.mobile.haml b/app/views/kaminari/_page.mobile.haml new file mode 100644 index 0000000..55c85ea --- /dev/null +++ b/app/views/kaminari/_page.mobile.haml @@ -0,0 +1,10 @@ +-# Link showing page number +-# available local variables +-# page: a page object for "this" page +-# url: url to this page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span{:class => "k_page#{' current' if page.current?}"} + = link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 0000000..3ebb206 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,18 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + .pagination + = first_page_tag unless current_page.first? + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? + = last_page_tag unless current_page.last? diff --git a/app/views/kaminari/_paginator.mobile.haml b/app/views/kaminari/_paginator.mobile.haml new file mode 100644 index 0000000..3ebb206 --- /dev/null +++ b/app/views/kaminari/_paginator.mobile.haml @@ -0,0 +1,18 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + .pagination + = first_page_tag unless current_page.first? + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? + = last_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 0000000..13f0d8a --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.prev + = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote diff --git a/app/views/kaminari/_prev_page.mobile.haml b/app/views/kaminari/_prev_page.mobile.haml new file mode 100644 index 0000000..13f0d8a --- /dev/null +++ b/app/views/kaminari/_prev_page.mobile.haml @@ -0,0 +1,9 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.prev + = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml new file mode 100644 index 0000000..ab808f6 --- /dev/null +++ b/app/views/layouts/admin.html.haml @@ -0,0 +1,28 @@ +!!! 5 +%html{:lang => 'zh-CN'} + %head + = render 'shared/head' + = stylesheet_link_tag "admin/i_base" + %body + #Top + = render 'shared/top', :link_class => :white + #Wrapper + #Content + .sep20 + #Sidebar + #AdminMenu + - dashboard_menu.each do |m| + .box.fix_cell + .cell + %strong.fade= m[:name] + - m[:items].each do |item| + .cell + = image_tag "admin/#{item.second}.png", :align => :absmiddle, :valign => :middle +   + = link_to item.first, item.last, :class => (:current_item if @title == item.first) + = yield :rightbar + #AdminContent + = render 'shared/content' + .c + .sep20 + = render 'shared/footer' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml new file mode 100644 index 0000000..7f6a85a --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -0,0 +1,27 @@ +!!! 5 +%html{:lang => 'zh-CN'} + %head + = render 'shared/head' + %body + #Top + = render 'shared/top', :link_class => :top + #Wrapper + - if Siteconf.global_banner.present? + .sep10 + #Banner + = image_tag Siteconf.global_banner, :alt => :banner + .sep10 + - else + .sep20 + #Content + #Sidebar + #Rightbar + = render 'shared/sidebar_box' + = yield :rightbar + = render :partial => 'shared/ad', :collection => Advertisement.available, :as => :ad + #Main + = render 'shared/content' + .c + .sep20 + = render 'shared/footer' + diff --git a/app/views/layouts/application.mobile.haml b/app/views/layouts/application.mobile.haml new file mode 100644 index 0000000..cbf1a5e --- /dev/null +++ b/app/views/layouts/application.mobile.haml @@ -0,0 +1,38 @@ +!!! 5 +%html{:lang => 'zh-CN'} + %head + = render :partial => 'shared/meta', :formats => :html + = stylesheet_link_tag "i_mobile" + = javascript_include_tag "application" + %body + #Top + #Member + - if user_signed_in? + %span.nickname= current_user.nickname + = user_profile_avatar_link(current_user, :mini, :class => :icon) +   + = link_to settings_path, :class => :icon do + #GearIcon +   + = link_to destroy_user_session_path, :method => :delete, :class => :icon do + #EjectIcon + - else + = link_to '登入', new_user_session_path, :class => :white +  |  + = link_to '注册', new_user_registration_path, :class => :white + = link_to Siteconf.site_name, root_path, :id => :Logo + #Main + - if @unread_count > 0 and @show_notification_count + .section 提醒 + .cell + = image_tag 'dot_orange.png', :class => :dot_icon, :align => :absmiddle + %strong + = link_to "#{@unread_count} 条未读提醒", notifications_path, :class => :notification + - if @breadcrumbs.length > 1 + .section + .fr= yield :nav_right + = build_breadcrumbs + = yield + .cell_bottom + = Siteconf.mobile_footer.html_safe + = render :partial => 'shared/google_analytics', :formats => :html diff --git a/app/views/nodes/_bookmark_node.html.haml b/app/views/nodes/_bookmark_node.html.haml new file mode 100644 index 0000000..3b19a9f --- /dev/null +++ b/app/views/nodes/_bookmark_node.html.haml @@ -0,0 +1,6 @@ +%tr + %td{:width => '50', :align => :right, :valign => :middle} + .howmany= node.topics_count + %td{:width => :auto, :align => :left} + %h3= link_to node.name, go_path(node.key) + diff --git a/app/views/nodes/_bookmark_node.mobile.haml b/app/views/nodes/_bookmark_node.mobile.haml new file mode 100644 index 0000000..c86348a --- /dev/null +++ b/app/views/nodes/_bookmark_node.mobile.haml @@ -0,0 +1 @@ +.cell= link_to node.name, go_path(node.key) diff --git a/app/views/nodes/_custom_fields.html.haml b/app/views/nodes/_custom_fields.html.haml new file mode 100644 index 0000000..722dab2 --- /dev/null +++ b/app/views/nodes/_custom_fields.html.haml @@ -0,0 +1,13 @@ +- if node.custom_css.present? + - content_for :final_head do + %style{:type => 'text/css'} + = node.custom_css.html_safe + +- if node.custom_html.present? + - content_for :rightbar do + .box + .inner + = node.custom_html.html_safe + .sep20 + + diff --git a/app/views/nodes/_item_node.html.haml b/app/views/nodes/_item_node.html.haml new file mode 100644 index 0000000..60c1449 --- /dev/null +++ b/app/views/nodes/_item_node.html.haml @@ -0,0 +1 @@ += link_to item_node.name, go_path(item_node.key), :class => :item_node diff --git a/app/views/nodes/_node.html.haml b/app/views/nodes/_node.html.haml new file mode 100644 index 0000000..e72a62b --- /dev/null +++ b/app/views/nodes/_node.html.haml @@ -0,0 +1 @@ += link_to node.name, go_path(node.key), :class => "item_node node_#{node.key}" diff --git a/app/views/nodes/_node.mobile.haml b/app/views/nodes/_node.mobile.haml new file mode 100644 index 0000000..7371086 --- /dev/null +++ b/app/views/nodes/_node.mobile.haml @@ -0,0 +1,2 @@ += link_to node.name, go_path(node.key), :style => 'font-size: 14px;' +   diff --git a/app/views/nodes/_paginator.html.haml b/app/views/nodes/_paginator.html.haml new file mode 100644 index 0000000..441cb46 --- /dev/null +++ b/app/views/nodes/_paginator.html.haml @@ -0,0 +1,18 @@ +- if @total_pages > 1 + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:width => 100, :align => :left} + - if @prev_page_num > 0 + .sep5 + = link_to '上一页', go_path(@node.key) + "?p=#{@prev_page_num}", :class => 'super normal button' + .sep5 + + %td{:width => :auto, :align => :center, :valign => :middle} + %strong{:style => 'font-size: 13px;'} + %span.fade= "#{@page_num}/#{@total_pages}" + %span.snow= "- #{@total_topics} Topics" + %td{:width => 100, :align => :right} + - if @next_page_num > 0 + .sep5 + = link_to '下一页', go_path(@node.key) + "?p=#{@next_page_num}", :class => 'super normal button' + .sep5 diff --git a/app/views/nodes/show.html.haml b/app/views/nodes/show.html.haml new file mode 100644 index 0000000..b6498f0 --- /dev/null +++ b/app/views/nodes/show.html.haml @@ -0,0 +1,44 @@ +- if user_signed_in? + - content_for :template_js do + $("a.super").click(function() { $("textarea.mll:first").focus() }); + +- if user_signed_in? and current_user.can_manage_site? + - content_for :rightbar do + .box + .cell + %strong 节点管理 + .cell + = link_to "修改 #{@node.name} 节点", admin_planes_path + "#!/click/edit_#{@node.html_id}", :class => 'op admin_op' + - if @node.quiet + .inner + = image_tag 'ghost.png', :align => :top, :title => t('tips.quiet_node') + = t('tips.quiet_node') + .sep20 + += render 'custom_fields', :node => @node + +.box + .header + .fr.f12 + %span.snow 话题总数 + %strong.gray= @total_topics + = build_navigation([@node.name], 'bigger') + - if @node.introduction.present? + .sep10 + %span.fade= @node.introduction + - if user_signed_in? + .sep10 + = link_to '创建新话题', '#new_topic', :class => 'super normal button' + .sep5 + = render @topics + .inner + = render 'paginator' + +- if user_signed_in? + .sep20 + .box + .inner + %h3 创建新话题 + = render 'topics/form', :node => @node, :topic => @node.topics.new, :comments_closed => false + + diff --git a/app/views/nodes/show.mobile.haml b/app/views/nodes/show.mobile.haml new file mode 100644 index 0000000..f3ad124 --- /dev/null +++ b/app/views/nodes/show.mobile.haml @@ -0,0 +1,16 @@ +- content_for :nav_right do + = @total_topics + 个主题 + +- if user_signed_in? + .cell + = link_to new_node_topic_path(@node), :class => :btn do + %input{:type => :button, :value => '创建新主题'} + += render @topics +- if @total_pages > 1 + .cell{:align => :center} + = render :partial => 'paginator', :formats => :html + +- if @node.custom_html.present? + .cell= @node.custom_html.html_safe diff --git a/app/views/notifications/_notification.mobile.haml b/app/views/notifications/_notification.mobile.haml new file mode 100644 index 0000000..143692f --- /dev/null +++ b/app/views/notifications/_notification.mobile.haml @@ -0,0 +1,5 @@ +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + = render :partial => 'shared/notification', :formats => :html, :locals => {:notification => notification} + + diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml new file mode 100644 index 0000000..503cb0e --- /dev/null +++ b/app/views/notifications/index.html.haml @@ -0,0 +1,6 @@ +.box + .cell + = build_navigation([@title]) + .inner + %table{:cellpadding => 0, :cellspacing => 0, :border => 0} + = render :partial => 'shared/notification', :collection => @notifications diff --git a/app/views/notifications/index.mobile.haml b/app/views/notifications/index.mobile.haml new file mode 100644 index 0000000..49b506e --- /dev/null +++ b/app/views/notifications/index.mobile.haml @@ -0,0 +1 @@ += render @notifications diff --git a/app/views/pages/_nav.html.haml b/app/views/pages/_nav.html.haml new file mode 100644 index 0000000..784e966 --- /dev/null +++ b/app/views/pages/_nav.html.haml @@ -0,0 +1,6 @@ +- if nav.content.start_with?('http') + = link_to nav.title, nav.content, :class => 'dark nav', :target => :_blank +- elsif nav.content.start_with?('/') + = link_to nav.title, nav.content, :class => 'dark nav' +- else + = link_to nav.title, page_path(nav.key), :class => 'dark nav' diff --git a/app/views/pages/_nav_ruler.html.haml b/app/views/pages/_nav_ruler.html.haml new file mode 100644 index 0000000..09d6eff --- /dev/null +++ b/app/views/pages/_nav_ruler.html.haml @@ -0,0 +1 @@ +%span.divider   |   diff --git a/app/views/pages/show.html.haml b/app/views/pages/show.html.haml new file mode 100644 index 0000000..31b1c2c --- /dev/null +++ b/app/views/pages/show.html.haml @@ -0,0 +1,15 @@ +.box + .cell + = build_navigation([@title]) + .inner + - if current_user && current_user.can_manage_site? + .fr + = link_to '修改', edit_admin_page_path(@page), :class => :op + .page + %article + %h1 + = @title + - unless @page.published + %small (草稿) + = format_page @page.content + diff --git a/app/views/pages/show.mobile.haml b/app/views/pages/show.mobile.haml new file mode 100644 index 0000000..85eb3e7 --- /dev/null +++ b/app/views/pages/show.mobile.haml @@ -0,0 +1,4 @@ +- add_breadcrumb(@title) +.cell + %h3= @title + = format_page @page.content diff --git a/app/views/planes/_plane.html.haml b/app/views/planes/_plane.html.haml new file mode 100644 index 0000000..b216b65 --- /dev/null +++ b/app/views/planes/_plane.html.haml @@ -0,0 +1,7 @@ +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:align => :right, :width => 80} + %span.fade= plane.name + %td{:style => 'line-height: 200%; padding-left: 15px;'} + = render plane.cached_assoc_collection(:nodes, Node.default_order_str, 20) diff --git a/app/views/planes/_plane.mobile.haml b/app/views/planes/_plane.mobile.haml new file mode 100644 index 0000000..00b758a --- /dev/null +++ b/app/views/planes/_plane.mobile.haml @@ -0,0 +1,7 @@ +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0} + %tr + %td{:align => :right, :width => 60} + %span.fade= plane.name + %td{:style => "line-height: 200%; padding-left: 10px;"} + = render plane.nodes.default_order.limit(20) diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml new file mode 100644 index 0000000..dbd36b8 --- /dev/null +++ b/app/views/sessions/new.html.haml @@ -0,0 +1,28 @@ +.box + .cell= build_navigation(['登入']) + .inner + = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| + = devise_error_messages! + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 80, :align => :right} 用户名 + %td{:width => 200, :align => :left} + = f.text_field :nickname, :class => :sl, :autofocus => true + %tr + %td{:width => 80, :align => :right} 密码 + %td{:width => 200, :align => :left} + = f.password_field :password, :class => :sl + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + - if devise_mapping.rememberable? + .hide= f.check_box :remember_me, :checked => true + = f.submit '登入', :class => 'super normal button' + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + %span.fade 登录后 cookie 会被记住一年 + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + = render :partial => "devise/shared/links" diff --git a/app/views/sessions/new.mobile.haml b/app/views/sessions/new.mobile.haml new file mode 100644 index 0000000..e1e0a1c --- /dev/null +++ b/app/views/sessions/new.mobile.haml @@ -0,0 +1,24 @@ +- @title = '登入' +- add_breadcrumb @title +.cell + = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| + = devise_error_messages! + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 80, :align => :right} 用户名 + %td{:width => 200, :align => :left} + = f.text_field :nickname, :class => :sl, :autofocus => true + %tr + %td{:width => 80, :align => :right} 密码 + %td{:width => 200, :align => :left} + = f.password_field :password, :class => :sl + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + - if devise_mapping.rememberable? + .hide= f.check_box :remember_me, :checked => true + = f.submit '登入', :class => 'super normal button' + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + %span.fade 登录后 cookie 会被记住一年 diff --git a/app/views/shared/_ad.html.haml b/app/views/shared/_ad.html.haml new file mode 100644 index 0000000..ba38c8b --- /dev/null +++ b/app/views/shared/_ad.html.haml @@ -0,0 +1,15 @@ +.box + .inner{:align => :center} + %div{:style => 'width: 240px; text-align: left'} + %span{:style => 'font-weight: bold; font-size: 10px; color: #e2e2e2;'} Promotion + .sep10 + / TODO: Record outbound ad link + = link_to ad.link, :class => :track_event, :target => '_blank', :data => {:category => :ad, :action => :click, :label => ad.title} do + = image_tag ad.banner.url, :border => 0, :width => 120, :height => 90 + .sep5 + %span{:style => 'font-size: 11px; color: #666;'} + %strong{:style => 'color: #000;'} + = link_to ad.title, ad.link, :target => '_blank', :class => 'black track_event', :data => {:category => :ad, :action => :click, :label => ad.title} + .sep3 + %span{:style => 'font-size: 12px;'}= ad.words +.sep20 diff --git a/app/views/shared/_box_tip.html.haml b/app/views/shared/_box_tip.html.haml new file mode 100644 index 0000000..016d200 --- /dev/null +++ b/app/views/shared/_box_tip.html.haml @@ -0,0 +1,3 @@ +.glass + .inner.center + = tip diff --git a/app/views/shared/_community_stats.html.haml b/app/views/shared/_community_stats.html.haml new file mode 100644 index 0000000..60b1a23 --- /dev/null +++ b/app/views/shared/_community_stats.html.haml @@ -0,0 +1,21 @@ +.box + .cell + 社区运行状态 + .inner + %table{:cellpadding => 3, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:align => :right, :width => 50} + %span.fade 注册会员 + %td{:align => :left} + %strong= User.cached_count + %tr + %td{:align => :right, :width => 50} + %span.fade 话题 + %td{:align => :left} + %strong= Topic.cached_count + %tr + %td{:align => :right, :width => 50} + %span.fade 回复 + %td{:align => :left} + %strong= Comment.cached_count +.sep20 diff --git a/app/views/shared/_content.html.haml b/app/views/shared/_content.html.haml new file mode 100644 index 0000000..b5f70a5 --- /dev/null +++ b/app/views/shared/_content.html.haml @@ -0,0 +1,7 @@ +- if flash_messages.any? + .box + .inner + = show_flash_messages + .sep20 += yield + diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml new file mode 100644 index 0000000..6c213cf --- /dev/null +++ b/app/views/shared/_footer.html.haml @@ -0,0 +1,21 @@ +#Bottom + #BottomMain + %strong + = render :partial => 'pages/nav', :collection => Page.only_published.default_order, :spacer_template => 'pages/nav_ruler' + = Siteconf.footer.html_safe + .sep10 + %small + Powered by + = link_to 'Rabel', 'http://rabelapp.com', :style => 'color: #E2E2E2', :target => '_blank' + = Rabel.version + += render 'shared/google_analytics' + +:javascript + jQuery(function($) { + window.rabel.search_engine_url = "#{search_engine_url}"; + #{yield :template_js} + }); + +:javascript + #{Siteconf.custom_js.html_safe} diff --git a/app/views/shared/_google_analytics.html.haml b/app/views/shared/_google_analytics.html.haml new file mode 100644 index 0000000..5e72fce --- /dev/null +++ b/app/views/shared/_google_analytics.html.haml @@ -0,0 +1,12 @@ +- if Rails.env.production? and Siteconf.ga_id.present? + %script{:type => 'text/javascript'} + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '#{Siteconf.ga_id}']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); +- else + / Google Analytics tracking code goes here diff --git a/app/views/shared/_head.html.haml b/app/views/shared/_head.html.haml new file mode 100644 index 0000000..e32620c --- /dev/null +++ b/app/views/shared/_head.html.haml @@ -0,0 +1,9 @@ += render 'shared/meta' += stylesheet_link_tag "application" += javascript_include_tag "application" += yield :head +- if Siteconf.theme != 'default' + = stylesheet_link_tag "#{Siteconf.theme}/i_theme" +- if Siteconf.custom_css.present? + %style{:type => 'text/css'}= Siteconf.custom_css.html_safe += yield :final_head diff --git a/app/views/shared/_meta.html.haml b/app/views/shared/_meta.html.haml new file mode 100644 index 0000000..a20d0b5 --- /dev/null +++ b/app/views/shared/_meta.html.haml @@ -0,0 +1,8 @@ +%title= title +%meta{:charset => 'UTF-8'} +%meta{:name => 'HandheldFriendly', :content => 'True'} +%meta{:content => "width=device-width, initial-scale=1.0", :name => :viewport} += csrf_meta_tags += Siteconf.custom_head_tags.html_safe += favicon_link_tag += auto_discovery_link_tag :atom, topics_path(:atom) diff --git a/app/views/shared/_my_fav.html.haml b/app/views/shared/_my_fav.html.haml new file mode 100644 index 0000000..f1e6ed1 --- /dev/null +++ b/app/views/shared/_my_fav.html.haml @@ -0,0 +1,11 @@ +%tr + %td.with_separator{:width => '34%', :align => :center} + = link_to my_topics_path, :class => :dark, :style => 'display: block;' do + %span.bigger= current_user.bookmarked_topics_count + .sep3 + %span.fade 话题收藏 + %td{:width => '33%', :align => :center} + = link_to my_following_path, :class => :dark, :style => 'display: block;' do + %span.bigger= current_user.followed_user_count + .sep3 + %span.fade 特别关注 diff --git a/app/views/shared/_notification.html.haml b/app/views/shared/_notification.html.haml new file mode 100644 index 0000000..38a679a --- /dev/null +++ b/app/views/shared/_notification.html.haml @@ -0,0 +1,24 @@ +- action_user = notification.action_user +%tr + %td{:width => 32, :align => :left, :valign => :top} + = user_profile_avatar_link(action_user, :mini) + %td{:valign => :top} + %span.fade + %strong= user_profile_link(action_user) + - if notification.action == Notification::ACTION_REWARD + - if notification.notifiable.amount > 0 + %span.green 奖励给你 + - else + %span.red 从你的帐户中扣除了 + %strong= notification.notifiable.amount.abs + = Siteconf.reward_title + - else + = notification.action_info_prefix + = link_to notification.notifiable.notifiable_title, read_notification_path(notification) + = notification.action_info_suffix + '了你' + %span.snow + = time_ago_in_words(notification.created_at) + .sep5 + - if notification.content.present? + .payload= parse_markdown(notification.content) + diff --git a/app/views/shared/_preview_widget.html.haml b/app/views/shared/_preview_widget.html.haml new file mode 100644 index 0000000..dcca2f5 --- /dev/null +++ b/app/views/shared/_preview_widget.html.haml @@ -0,0 +1,3 @@ += link_to '预览', 'javascript:void(0);', :class => 'action_label preview', :data => {:ref => ref, :type => type} += link_to '返回修改', 'javascript:void(0);', :class => 'action_label cancel_preview hide', :data => {:ref => ref} +#preview diff --git a/app/views/shared/_rss.html.haml b/app/views/shared/_rss.html.haml new file mode 100644 index 0000000..6693de6 --- /dev/null +++ b/app/views/shared/_rss.html.haml @@ -0,0 +1,2 @@ += image_tag 'rss.png', :align => :absmiddle += link_to 'RSS', topics_path(:atom), :target => '_blank', :class => :dark diff --git a/app/views/shared/_sidebar_box.html.haml b/app/views/shared/_sidebar_box.html.haml new file mode 100644 index 0000000..d44938c --- /dev/null +++ b/app/views/shared/_sidebar_box.html.haml @@ -0,0 +1,42 @@ +- if user_signed_in? + .box + .cell + %table + %tr + %td{:width => 48, :valign => :top} + = user_profile_avatar_link(current_user, :medium) + %td{:width => 10, :valign => :top} + %td{:width => :auto, :valign => :left} + %span.bigger= user_profile_link(current_user) + .sep5 + %span.fade= current_user.account.signature + .sep10 + %table + = render 'shared/my_fav' + .inner + - if @unread_count > 0 + = image_tag 'dot_orange.png', :class => :icon, :align => :top + = link_to "#{@unread_count} 条未读提醒", notifications_path, :class => :fade + - else + %span.fade 暂无提醒 + - unless current_user.has_avatar? + .yellow + %span.fade + 头像不够个性? + %a{:href => settings_path + "#avatar"} 立刻上传 → +- else + .box + .cell + = "#{Siteconf.site_name} — #{Siteconf.short_intro}" + .inner + .sep5 + .center + = link_to '现在注册', new_user_registration_path, :class => 'super normal button' + .sep5 + .sep10 + 已注册用户请 + = link_to '登入', new_user_session_path +.sep20 + +- content_for :rightbar do + = Siteconf.global_sidebar_block.html_safe diff --git a/app/views/shared/_top.html.haml b/app/views/shared/_top.html.haml new file mode 100644 index 0000000..774ec82 --- /dev/null +++ b/app/views/shared/_top.html.haml @@ -0,0 +1,21 @@ +#TopMain + - if Siteconf.custom_logo.present? + = link_to root_path, :class => 'logo custom_logo' do + = image_tag Siteconf.custom_logo, :alt => :logo + - else + = link_to Siteconf.site_name, root_path, :class => 'logo text_logo' + #Navigation + %ul + %li= link_to '首页', root_path, :class => link_class + - if user_signed_in? + %li= user_profile_link(current_user, :class => link_class) + %li= link_to '个人设置', settings_path, :class => link_class + - if current_user.can_manage_site? + %li= link_to '管理后台', admin_root_path, :class => link_class + %li= link_to '登出', destroy_user_session_path, :method => :delete, :class => link_class + - else + %li= link_to '注册', new_user_registration_path, :class => link_class + %li= link_to '登入', new_user_session_path, :class => link_class + #Search + .search_input + %input#q{:type => 'text', :maxlength => 40, :name => 'q', :data => {:domain => Siteconf.site_host}} diff --git a/app/views/topics/_back_to_node.html.haml b/app/views/topics/_back_to_node.html.haml new file mode 100644 index 0000000..15b1353 --- /dev/null +++ b/app/views/topics/_back_to_node.html.haml @@ -0,0 +1,4 @@ +%span.fade + %span.chevron ‹  + 返回 + = link_to node.name, go_path(node.key) diff --git a/app/views/topics/_complex_topic.html.haml b/app/views/topics/_complex_topic.html.haml new file mode 100644 index 0000000..2e6a9b7 --- /dev/null +++ b/app/views/topics/_complex_topic.html.haml @@ -0,0 +1,28 @@ +- comments_count = topic.comments_count +- last_replied_by = topic.last_replied_by +.cell.topic{:class => topic_user.can_manage_site? ? 'admin' : ''} + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:valign => :top, :class => :avatar} + = user_profile_avatar_link(topic_user, :medium) + %td{:valign => :top, :style => 'padding-left: 12px'} + - if comments_count > 0 + .fr + = link_to comments_count, t_path(topic.id), :class => :count + .sep3 + %span.bigger{:style => 'font-size: 16px; line-height: 130%'} + = link_to topic.title, t_path(topic.id), :class => :topic + .sep5 + %span.created + %strong= link_to topic_node.name, go_path(topic_node.key), :class => :node +   •   + %strong= user_profile_link(topic_user, :class => :dark) + - if comments_count > 0 +   •   + = time_ago_in_words(topic.last_replied_at) +   •   + 最后回复来自 + = nickname_profile_link(last_replied_by) + - else +   •   + = time_ago_in_words(topic.created_at) diff --git a/app/views/topics/_form.html.haml b/app/views/topics/_form.html.haml new file mode 100644 index 0000000..3387727 --- /dev/null +++ b/app/views/topics/_form.html.haml @@ -0,0 +1,17 @@ +%p.fade 提示: 如果标题已经包含你想说的话,内容可以留空。 += form_for [node, topic] do |f| + %a{:name => 'new_topic'} + = f.label :title, '标题' + .sep5 + = f.text_area :title, :class => :mll, :style => 'height: 10px;', :maxlength => 150 + .sep10 + = render 'shared/preview_widget', :ref => :topic_content, :type => :topic + = f.text_area :content, :class => :mll + .sep5 + - if current_user.can_manage_site? + = f.check_box :sticky + = f.label :sticky, '保持置顶', :class => :small + = f.check_box :comments_closed + = f.label :comments_closed, '禁止回复', :class => :small + .sep5 + = f.submit (topic.new_record? ? '创建' : '提交修改'), :class => 'super normal button', :data => {:disable_with => t('tips.submitting')} diff --git a/app/views/topics/_move_form.js.haml b/app/views/topics/_move_form.js.haml new file mode 100644 index 0000000..808ed5e --- /dev/null +++ b/app/views/topics/_move_form.js.haml @@ -0,0 +1,6 @@ += form_for [@node, @topic], :remote => :true do |f| + = label_tag '移动到新节点' + %br + = select_tag "new_node_id", option_groups_from_collection_for_select(Plane.default_order.all, :nodes, :name, :id, :name, f.object.node.id) + %br + = f.submit '开始移动', :class => 'super normal button' diff --git a/app/views/topics/_profile_topic.mobile.haml b/app/views/topics/_profile_topic.mobile.haml new file mode 100644 index 0000000..808ebf4 --- /dev/null +++ b/app/views/topics/_profile_topic.mobile.haml @@ -0,0 +1,11 @@ +.cell + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 80, :align => :right} + = link_to topic.node.name, go_path(topic.node.key) + %td{:align => :left} + = link_to topic.title, t_path(topic.id) + %small.fade + 收到 + = topic.comments_count + 回复 diff --git a/app/views/topics/_simple_topic.html.haml b/app/views/topics/_simple_topic.html.haml new file mode 100644 index 0000000..35a08f7 --- /dev/null +++ b/app/views/topics/_simple_topic.html.haml @@ -0,0 +1,14 @@ +- comments_count = topic.comments_count +.cell.topic{:class => topic_user.can_manage_site? ? 'admin' : ''} + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:valign => :top, :class => :avatar} + = user_profile_avatar_link(topic_user, :mini) + %td{:valign => :top, :style => 'padding-left: 12px'} + - if comments_count > 0 + .fr + = link_to comments_count, t_path(topic.id), :class => :count + .sep3 + %span.bigger{:style => 'font-size: 16px; line-height: 130%'} + = link_to topic.title, t_path(topic.id), :class => :topic + diff --git a/app/views/topics/_table_view.html.haml b/app/views/topics/_table_view.html.haml new file mode 100644 index 0000000..5f117bc --- /dev/null +++ b/app/views/topics/_table_view.html.haml @@ -0,0 +1,27 @@ +%table{:cellpadding => 5, :cellspacing => 0, :border => 0, :width => '100%', :class => :topics} + %tr + %th{:align => :right, :width => 50} 回复 + %th{:align => :left, :width => :auto} 标题 + %th{:align => :left, :width => 200, :colspan => 2} 最后回复时间 + - i = 1 + - topics.each do |topic| + - class_name = (i % 2 == 0) ? 'even' : 'odd' + - i += 1 + %tr + %td{:align => :right, :width => 50, :class => "#{class_name} lend"} + - comments_count = topic.comments_count + - if comments_count > 0 + %strong + %span.green= comments_count + - else + %span.snow 0 + %td{:align => :left, :width => :auto, :class => class_name} + = link_to topic.title, t_path(topic.id) + - last_comment = topic.last_comment + - last_comment = topic if last_comment.nil? + %td{:align => :left, :width => 80, :class => class_name} + = user_profile_link(last_comment.user, :class => :dark) + %td{:align => :left, :width => 120, :class => "#{class_name} rend"} + %small.fade= last_comment.created_at.strftime("%Y-%m-%d %H:%M:%S") + + diff --git a/app/views/topics/_title_form.html.haml b/app/views/topics/_title_form.html.haml new file mode 100644 index 0000000..e316ec9 --- /dev/null +++ b/app/views/topics/_title_form.html.haml @@ -0,0 +1,7 @@ += form_for [@node, @topic], :url => update_title_node_topic_path(@node, @topic), :remote => true do |f| + %a{:name => 'new_topic'} + = f.label :title, '标题' + .sep5 + = f.text_area :title, :class => :ml, :autofocus => true + .sep10 + = f.submit '提交修改', :class => 'super normal button', :data => {:disable_with => t('tips.submitting')} diff --git a/app/views/topics/_topic.html.haml b/app/views/topics/_topic.html.haml new file mode 100644 index 0000000..7676bf6 --- /dev/null +++ b/app/views/topics/_topic.html.haml @@ -0,0 +1,5 @@ +- topic_user = topic.cached_assoc_object(:user) +- topic_node = topic.cached_assoc_object(:node) +- comments_count = topic.comments_count += render "topics/#{Siteconf.topic_list_style}_topic", :topic_user => topic_user, :topic_node => topic_node, :topic => topic + diff --git a/app/views/topics/_topic.mobile.haml b/app/views/topics/_topic.mobile.haml new file mode 100644 index 0000000..7b59552 --- /dev/null +++ b/app/views/topics/_topic.mobile.haml @@ -0,0 +1,24 @@ +- topic_user = topic.user +- comment_count = topic.comments_count +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.avatar{:valign => :top} + = user_profile_avatar_link(topic_user, :medium) + %td{:valign => :top, :style => 'padding-left: 8px;'} + %span.fade + = user_profile_link(topic_user) +  in  + = link_to topic.node.name, go_path(topic.node.key) + - if comment_count > 0 + %small + 收到 + = comment_count + 回复 + .sep5 + %span.bigger + = link_to topic.title, t_path(topic.id) + %span.created + = time_ago_in_words(topic.created_at) + + diff --git a/app/views/topics/edit.html.haml b/app/views/topics/edit.html.haml new file mode 100644 index 0000000..89527f0 --- /dev/null +++ b/app/views/topics/edit.html.haml @@ -0,0 +1,32 @@ +- unless @topic.locked? or current_user.can_manage_site? + .box + .cell + %span.fade 修改功能指南 + .inner + 在新主题创建后的 + = Siteconf.topic_editable_period_str + 分钟内,可以自由编辑。 + .sep5 + %strong + 距离本主题的编辑权限关闭还有  + %span.orange= (@topic.created_at + Siteconf.topic_editable_period - Time.now).round +  秒 + .sep20 + - content_for :template_js do + :plain + var second = parseInt($("span.orange").text()); + var countdown_id = setInterval(function() { + if (second == 0) { + $('#Rightbar strong').text('此主题编辑权限已经关闭'); + clearInterval(countdown_id); + return; + } + second = second - 1; + $("span.orange").text(second); + }, 1000); +.box + .cell= edit_topic_navigation(@node, @topic) + .cell + %h3 更新话题 + = render 'form', :node => @node, :topic => @topic, :comments_closed => @topic.comments_closed? + .inner= render 'back_to_node', :node => @node diff --git a/app/views/topics/edit_title.js.haml b/app/views/topics/edit_title.js.haml new file mode 100644 index 0000000..9c06c83 --- /dev/null +++ b/app/views/topics/edit_title.js.haml @@ -0,0 +1 @@ +$.facebox("#{escape_javascript(render('title_form'))}") diff --git a/app/views/topics/index.atom.builder b/app/views/topics/index.atom.builder new file mode 100644 index 0000000..d337607 --- /dev/null +++ b/app/views/topics/index.atom.builder @@ -0,0 +1,16 @@ +atom_feed :language => 'zh-CN' do |feed| + feed.title Siteconf.site_name + feed.updated @last_update + + @feed_items.each do |item| + feed.entry(item) do |entry| + entry.url t_url(item.id) + entry.title item.title + entry.content parse_markdown(item.content), :type => 'html' + entry.updated item.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ") + entry.author do |author| + author.name item.user.nickname + end + end + end +end diff --git a/app/views/topics/index.html.haml b/app/views/topics/index.html.haml new file mode 100644 index 0000000..1448c24 --- /dev/null +++ b/app/views/topics/index.html.haml @@ -0,0 +1,5 @@ +.box + .cell= build_navigation([@title]) + = render @topics + .inner{:align => :center} + = paginate @topics diff --git a/app/views/topics/move.js.haml b/app/views/topics/move.js.haml new file mode 100644 index 0000000..1264037 --- /dev/null +++ b/app/views/topics/move.js.haml @@ -0,0 +1 @@ +$.facebox("#{escape_javascript(render('move_form'))}") diff --git a/app/views/topics/new.html.haml b/app/views/topics/new.html.haml new file mode 100644 index 0000000..a7c694f --- /dev/null +++ b/app/views/topics/new.html.haml @@ -0,0 +1,6 @@ +.box + .cell + = build_navigation [link_to(@node.name, go_path(@node.key))] + .inner + %h2 创建新话题 + = render 'form', :node => @node, :topic => @topic, :comments_closed => false diff --git a/app/views/topics/new.mobile.haml b/app/views/topics/new.mobile.haml new file mode 100644 index 0000000..e1ed7aa --- /dev/null +++ b/app/views/topics/new.mobile.haml @@ -0,0 +1,14 @@ +- add_breadcrumb link_to(@node.name, go_path(@node.key), :class => :black) +- add_breadcrumb '新建主题' +.cell + = form_for [@node, @topic] do |f| + = f.text_field :title, :class => :sll + .sep5 + = f.text_area :content, :class => :mll + .sep5 + - if current_user.can_manage_site? + = check_box_tag :comments_closed + = label_tag :comments_closed, '禁止回复' + .sep5 + = f.submit '创建新主题', :class => :btn + diff --git a/app/views/topics/show.html.haml b/app/views/topics/show.html.haml new file mode 100644 index 0000000..367b862 --- /dev/null +++ b/app/views/topics/show.html.haml @@ -0,0 +1,56 @@ += render 'nodes/custom_fields', :node => @node += render 'topics/show/bookmarked_users' += render 'topics/show/manage' + +- content_for :template_js do + :plain + var creating_comment = false; + + $("textarea.mll").keydown(function(e) { + if (e.ctrlKey && e.keyCode == 13) { + if (creating_comment) return; + creating_comment = true + $("input[type=submit]").click(); + } + }); + +- topic_user = @topic.cached_assoc_object(:user) + +.box + %article + %div{:class => @topic.content.present? ? 'header' : 'inner'} + .fr + = user_profile_avatar_link(topic_user, :large) + = build_navigation([link_to(@node.name, go_path(@node.key))], 'bigger') + .sep10 + %h1#topic_title + = Rabel::Base.make_mention_links(Rabel::Base.h(@topic.title)).html_safe + %small.fade + By + = user_profile_link(topic_user, :class => :dark) + at + = time_ago_in_words(@topic.created_at) + , + = @topic.hit + 次浏览 + - if @topic.content.present? + .inner + .content.topic_content= format_topic(@topic.content) + - if user_signed_in? + .inner + .fr{:align => :right} + - if current_user.bookmarked?(@topic) + = link_to '取消收藏', current_user.bookmark_of(@topic), :method => :delete, :class => 'op unbookmark' + - else + = link_to '加入收藏', topic_bookmarks_path(@topic), :method => :post, :class => 'op bookmark' +    +.sep20 += render 'topics/show/comments' if @comments.any? + +- if @topic.comments_closed? + .sep20 + = render 'shared/box_tip', :tip => t('tips.comments_closed') +- elsif @comments.empty? + = render 'shared/box_tip', :tip => '目前尚无回复' + += render 'topics/show/comment_form' unless @topic.comments_closed? diff --git a/app/views/topics/show.mobile.haml b/app/views/topics/show.mobile.haml new file mode 100644 index 0000000..a9840ae --- /dev/null +++ b/app/views/topics/show.mobile.haml @@ -0,0 +1,52 @@ +- add_breadcrumb link_to(@node.name, go_path(@node.key), :class => :black) +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.avatar{:valign => :top} + = user_profile_avatar_link(@topic.user, :medium) + %td{:valign => :top, :style => 'padding-left: 5px;'} + %h1= @topic.title + .sep5 + %span.fade + By + = user_profile_link(@topic.user) + = time_ago_in_words(@topic.created_at) + - if @topic.hit > 0 + = " - #{@topic.hit}次点击" + .sep10 + = format_topic @topic.content +.cell + .fr + - if @total_bookmarks > 0 + %small.fade= "已有 #{@total_bookmarks} 人收藏" +   + - if current_user + - if current_user.bookmarked?(@topic) + = link_to '取消收藏', current_user.bookmark_of(@topic), :method => :delete +   + = image_tag 'heart.png', :align => :top + - else + = link_to '加入收藏', topic_bookmarks_path(@topic), :method => :post + + %strong.fade + - if @topic.comments_closed? + = t('tips.comments_closed') + - else + 共收到 + = @topic.comments_count + 条回复 + +#replies + = render @comments + - if @total_pages > 1 + .cell{:align => :center} + = paginate @comments, :param_name => :p, :window => 2 + +- if user_signed_in? and not @topic.comments_closed? + .cell + %span.fade 现在就添加一条回复 + = form_for [@topic, @new_comment] do |f| + .sep5 + = f.text_area :content, :class => :mll + .sep5 + = f.submit '发送' diff --git a/app/views/topics/show/_bookmark_button.html.haml b/app/views/topics/show/_bookmark_button.html.haml new file mode 100644 index 0000000..e01f126 --- /dev/null +++ b/app/views/topics/show/_bookmark_button.html.haml @@ -0,0 +1,8 @@ +- if user_signed_in? + .inner + .fr{:align => :right} + - if current_user.bookmarked?(@topic) + = link_to '取消收藏', current_user.bookmark_of(@topic), :method => :delete, :class => 'op cancel' + - else + = link_to '加入收藏', topic_bookmarks_path(@topic), :method => :post, :class => :op +    diff --git a/app/views/topics/show/_bookmarked_users.html.haml b/app/views/topics/show/_bookmarked_users.html.haml new file mode 100644 index 0000000..547811f --- /dev/null +++ b/app/views/topics/show/_bookmarked_users.html.haml @@ -0,0 +1,9 @@ +- if @total_bookmarks > 0 + - content_for :rightbar do + .box + .cell + %span.fade 收藏此话题的成员 + .inner + - @topic.bookmarks.each do |b| + = user_profile_avatar_link(b.user, :mini) + .sep20 diff --git a/app/views/topics/show/_comment_form.html.haml b/app/views/topics/show/_comment_form.html.haml new file mode 100644 index 0000000..7f2343f --- /dev/null +++ b/app/views/topics/show/_comment_form.html.haml @@ -0,0 +1,15 @@ +- if user_signed_in? + .sep20 + = form_for [@topic, @new_comment] do |f| + .box + .cell + .fr.fade + ⬆ + = link_to '回到顶部', 'javascript:void(0);', :class => 'dark back_to_top' + .fade 现在就添加一条回复 + .inner + = render 'shared/preview_widget', :ref => :comment_content, :type => :comment + = f.text_area :content, :class => :mll + .sep10 + = f.submit '发送', :class => 'super normal button', :data => {:disable_with => t('tips.submitting')} + %small.fade 支持 Ctrl + Enter 快捷键 diff --git a/app/views/topics/show/_comments.html.haml b/app/views/topics/show/_comments.html.haml new file mode 100644 index 0000000..a3c4bed --- /dev/null +++ b/app/views/topics/show/_comments.html.haml @@ -0,0 +1,16 @@ +%section + .box + .cell + .fr.fade + - if @topic.comments_closed? + 回复权限关闭 + - else + ⬇ + = link_to '跳到回复', 'javascript:void(0);', :class => 'dark jump_to_comment' + %span.fade + = "#{@total_comments} 回复" + #replies{:class => "#{'fix_cell' if @total_pages == 1}"} + = render @comments + - if @total_pages > 1 + .inner{:align => :center} + = paginate @comments, :param_name => :p, :window => 3 diff --git a/app/views/topics/show/_manage.html.haml b/app/views/topics/show/_manage.html.haml new file mode 100644 index 0000000..07c44ce --- /dev/null +++ b/app/views/topics/show/_manage.html.haml @@ -0,0 +1,19 @@ +- if can? :update, @topic + - content_for :rightbar do + .box + .cell + %span.fade 话题管理 + .cell + = link_to '修改标题', edit_title_node_topic_path(@node, @topic), :remote => true, :class => 'op admin_op' + = link_to '编辑全部', edit_node_topic_path(@node, @topic), :class => 'op admin_op' + .cell + = link_to '移动到新节点', move_node_topic_path(@node, @topic), :remote => true, :class => 'op admin_op' + - if current_user.can_manage_site? + .cell + - toggle_comments_closed_tip = @topic.comments_closed? ? '允许回复' : '禁止回复' + = link_to toggle_comments_closed_tip, topic_toggle_comments_closed_path(@topic), :method => :put, :class => 'op admin_op' + - toggle_sticky_tip = @topic.sticky? ? '取消置顶' : '置顶此话题' + = link_to toggle_sticky_tip, topic_toggle_sticky_path(@topic), :method => :put, :class => 'op admin_op' + .inner + = link_to '删除此话题', node_topic_path(@node, @topic), :method => :delete, :data => {:confirm => t(:delete_confirm)}, :class => 'op admin_op op_danger' + .sep20 diff --git a/app/views/topics/update_title.js.haml b/app/views/topics/update_title.js.haml new file mode 100644 index 0000000..e8c74f1 --- /dev/null +++ b/app/views/topics/update_title.js.haml @@ -0,0 +1,2 @@ +$("#topic_title").html("#{escape_javascript(@topic.title)}"); +$.facebox.close(); diff --git a/app/views/users/_account_detail_form.html.haml b/app/views/users/_account_detail_form.html.haml new file mode 100644 index 0000000..4e3e5ac --- /dev/null +++ b/app/views/users/_account_detail_form.html.haml @@ -0,0 +1,22 @@ += f.fields_for :account do |fields| + %tr + %td{:width => 120, :align => :right} 个人网站 + %td{:width => 200, :align => :left} + = fields.text_field :personal_website, :class => :sl + %tr + %td{:width => 120, :align => :right} 所在地 + %td{:width => 200, :align => :left} + = fields.text_field :location, :class => :sl + %tr + %td{:width => 120, :align => :right} 签名 + %td{:width => 200, :align => :left} + = fields.text_field :signature, :class => :sl + %tr + %td{:width => 120, :align => :right} 个人简介 + %td{:width => 200, :align => :left} + = fields.text_area :introduction, :class => :ml + %tr + %td{:width => 120, :align => :right} 微博地址 + %td{:width => 200, :align => :left} + = fields.text_field :weibo_link, :class => :sl + diff --git a/app/views/users/_account_form.html.haml b/app/views/users/_account_form.html.haml new file mode 100644 index 0000000..13e6047 --- /dev/null +++ b/app/views/users/_account_form.html.haml @@ -0,0 +1,16 @@ += form_for @user, :url => update_account_path do |f| + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 120, :align => :right} 用户名 + %td{:width => 200, :align => :left} + = @user.nickname + %tr + %td{:width => 120, :align => :right} 电子邮件 + %td{:width => 200, :align => :left} + = f.email_field :email, :class => :sl + = render :partial => 'account_detail_form', :locals => {:f => f}, :formats => :html + + %tr + %td{:align => :right} + %td{:width => 200, :align => :left} + = f.submit '保存设置', :class => btn_class diff --git a/app/views/users/_followed_ruler.html.haml b/app/views/users/_followed_ruler.html.haml new file mode 100644 index 0000000..1e6259a --- /dev/null +++ b/app/views/users/_followed_ruler.html.haml @@ -0,0 +1,3 @@ +%tr + %td.ruler{:colspan => 2} + .ruler diff --git a/app/views/users/_followed_user.html.haml b/app/views/users/_followed_user.html.haml new file mode 100644 index 0000000..6c9477e --- /dev/null +++ b/app/views/users/_followed_user.html.haml @@ -0,0 +1,13 @@ +%tr + %td{:align => :left, :valign => :top, :width => 24} + = mini_avatar(followed_user) + %td{:align => :left, :valign => :top, :width => :auto, :style => 'padding-left: 10px;'} + = user_profile_link(followed_user) + - if followed_user.account.signature.present? + .sep3 + %small.fade= followed_user.account.signature + .sep3 + %small.fade + = followed_user.follower_count + followers + diff --git a/app/views/users/_password_form.html.haml b/app/views/users/_password_form.html.haml new file mode 100644 index 0000000..9e09e66 --- /dev/null +++ b/app/views/users/_password_form.html.haml @@ -0,0 +1,20 @@ += form_for @user, :url => update_password_path do |f| + %strong.fade 如果你不想更改密码,请留空以下输入框。 + .sep5 + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 120, :align => :right} 当前密码 + %td{:width => 200, :align => :left} + = f.password_field :current_password, :class => :sl + %tr + %td{:width => 120, :align => :right} 新密码 + %td{:width => 200, :align => :left} + = f.password_field :password, :class => :sl + %tr + %td{:width => 120, :align => :right} 新密码确认 + %td{:width => 200, :align => :left} + = f.password_field :password_confirmation, :class => :sl + %tr + %td{:align => :right} + %td{:width => 200, :align => :left} + = f.submit '更改密码', :class => btn_class diff --git a/app/views/users/_user.mobile.haml b/app/views/users/_user.mobile.haml new file mode 100644 index 0000000..08934cb --- /dev/null +++ b/app/views/users/_user.mobile.haml @@ -0,0 +1,9 @@ +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.avatar{:valign => :top} + = user_profile_avatar_link(user, :medium) + %td{:style => 'padding-left: 8px;', :valign => :top} + %h1= user_profile_link(user) + .sep5 + %p= user.account.signature diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100644 index 0000000..463e932 --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1,42 @@ +.box + .cell + = build_navigation(['设置']) + .inner + = render 'account_form', :btn_class => 'super normal button' +.sep20 + +%a{:name => :avatar}   +.box + .cell + %span.fade 头像 + .cell + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 120, :align => :right} 当前头像 + %td{:align => :left} + = large_avatar(current_user) +   + = medium_avatar(current_user) +   + = mini_avatar(current_user) + .inner + = form_for @user, :url => update_avatar_path, :html => {:multipart => true} do |f| + %span.fade 支持 1MB 以内的 PNG / GIF / JPG 图片文件作为头像,推荐使用正方形的透明 PNG 图片以获得最佳效果。 + .sep5 + %table{:cellpadding => 5, :cellspacing => 0, :border => 0} + %tr + %td{:width => 80, :align => :right} 选择图片文件 + %td{:width => 200, :align => :left} + = f.file_field :avatar + %tr + %td{:width => 80, :align => :right} + %td{:width => 200, :align => :left} + = f.submit '上传新头像', :class => 'super normal button' + +.sep20 +.box + .cell + %span.fade 安全 + .inner + = render 'users/password_form', :btn_class => 'super normal button' + diff --git a/app/views/users/edit.mobile.haml b/app/views/users/edit.mobile.haml new file mode 100644 index 0000000..e3b0e8e --- /dev/null +++ b/app/views/users/edit.mobile.haml @@ -0,0 +1,8 @@ +- add_breadcrumb @title += show_mobile_messages +.cell + = render :partial => 'users/account_form', :formats => :html, :locals => {:btn_class => ''} +.section 安全 +.cell + = render :partial => 'users/password_form', :formats => :html, :locals => {:btn_class => ''} += show_mobile_messages diff --git a/app/views/users/my_following.html.haml b/app/views/users/my_following.html.haml new file mode 100644 index 0000000..ee87872 --- /dev/null +++ b/app/views/users/my_following.html.haml @@ -0,0 +1,16 @@ +.box + .cell= build_navigation(['我的特别关注']) + .inner + %table{:cellspacing => 0, :cellpadding => 0, :border => 0, :width => '100%'} + %tr + %td.fix_cell{:align => :left, :valign => :top, :width => :auto} + = render @followed_topic_timeline + %td{:align => :left, :valign => :top, :width => 180, :class => :with_background} + .fr + %strong.snow= current_user.followed_user_count + %span.fade 我的特别关注 + .sep10 + %table{:cellspacing => 0, :cellpadding => 0, :border => 0, :width => '100%'} + = render :partial => 'users/followed_user', :collection => @my_followed_users, :spacer_template => 'users/followed_ruler' + + diff --git a/app/views/users/my_following.mobile.haml b/app/views/users/my_following.mobile.haml new file mode 100644 index 0000000..28788e5 --- /dev/null +++ b/app/views/users/my_following.mobile.haml @@ -0,0 +1,3 @@ += render @my_followed_users, :as => :user +.section 讨论主题 += render @followed_topic_timeline diff --git a/app/views/users/my_topics.html.haml b/app/views/users/my_topics.html.haml new file mode 100644 index 0000000..fc9a887 --- /dev/null +++ b/app/views/users/my_topics.html.haml @@ -0,0 +1,4 @@ +.box.fix_cell + .cell= build_navigation([@title]) + = render @my_topics + .inner diff --git a/app/views/users/my_topics.mobile.haml b/app/views/users/my_topics.mobile.haml new file mode 100644 index 0000000..2cacc19 --- /dev/null +++ b/app/views/users/my_topics.mobile.haml @@ -0,0 +1 @@ += render @my_topics diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100644 index 0000000..98bae23 --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1,112 @@ +- content_for :template_js do + $(document).bind('keydown', 'f', function() { $("a.follow").click() }); + $(document).bind('keydown', 'shift+f', function() { $("a.unfollow").click() }); + +- if @user.follower_count > 0 + - content_for :rightbar do + .box + .cell + = "关注#{@nickname_tip}的人" + %span.fade= "(#{@user.follower_count})" + .inner + - @user.recent_followers.each do |follower| + = user_profile_avatar_link(follower, :mini) + .sep20 + +.box + %div{:class => (@introduction.length > 0) ? 'cell' : 'inner'} + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:width => 73, :valign => :top, :align => :center} + = large_avatar(@user) + %td{:width => 10, :valign => :top} + %td{:width => :auto, :align => :left, :valign => :top} + .fr + .sep3 + - if @user.reward > 0 + %span.fade + %strong#reward_balance= @user.reward + = Siteconf.reward_title + - if user_signed_in? and @user != current_user + - if current_user.following?(@user) + = link_to '取消特别关注', unfollow_user_path(@user.nickname), :method => :post, :class => 'super inverse button unfollow' + - else + = link_to '加入特别关注', follow_user_path(@user.nickname), :method => :post, :class => 'super special button follow' + %h2{:style => 'padding: 0px; margin: 0px; font-size: 22px; line-height: 22px;'} + = @user.nickname + - if @signature.length > 0 + .sep5 + %span.fade.bigger= @signature + .sep5 + %span.snow + = Siteconf.site_name + 第 + = @user.id + 号会员, 加入于 + = @user.created_at.strftime("%Y-%m-%d %H:%M:%S %p") + .sep10 + %table{:cellpadding => 2, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:width => '50%'} + - if @weibo_link.length > 0 + %span{:style => 'line-height: 16px;'} + = image_tag "icon/#{weibo_icon_for(@weibo_link)}.png", :align => :absmiddle +   + = link_to @weibo_link, @weibo_link, :target => :_blank, :rel => 'nofollow external' + %tr + %td{:width => '50%'} + - if @personal_website.length > 0 + %span{:style => 'line-height: 16px;'} + = image_tag 'icon/mobileme.png', :align => :absmiddle +   + = link_to @personal_website, @personal_website, :target => '_blank', :rel => 'nofollow external' + %tr + %td{:width => '50%'} + - if @location.length > 0 + %span{:style => 'line-height: 16px;'} + = image_tag 'icon/location.png', :align => :absmiddle +   + = link_to @location, "http://www.google.com/maps?q=#{@location}", :target => '_blank', :rel => 'nofollow external' + + - if @introduction.length > 0 + .inner= parse_markdown @introduction + + - if user_signed_in? and current_user.can_manage_site? + .inner + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td{:width => 73, :valign => :top, :align => :center} + %td{:width => :auto} + = link_to "奖励#{Siteconf.reward_title}", new_admin_user_reward_path(@user), :class => 'op admin_op', :remote => true + = link_to "扣除#{Siteconf.reward_title}", new_admin_user_reward_path(@user) + "?reward_type=#{Reward::TYPE_REVOKE}", :class => 'op admin_op', :remote => true + = link_to '管理此用户', admin_users_path + "?nickname=#{url_encode(@user.nickname)}", :class => 'op admin_op' + +.sep20 +.box + .cell + %span.fade 最近创建主题 + .cell + = render :partial => 'topics/table_view', :locals => { :topics => @user.latest_created_topics } + - if @user.topics.any? + .inner + %span.chevron » + = link_to "#{@user.nickname} 创建的更多主题", member_topics_path(@user.nickname) + +.sep20 +.box + .cell + %span.fade 最近参与主题 + .inner + = render :partial => 'topics/table_view', :locals => { :topics => @user.latest_replied_topics } + +- if user_signed_in? and current_user.can_manage_site? and (not @user.root?) and @user != current_user + .sep20 + .box + .cell + %a{:name => :spam} + %strong.danger SPAM 处理 + .inner + %p 如果该用户违反了社区行为准则,您可以删除此会员。 + %p 与该会员相关的所有信息,也会一并删除。 + .inner + = link_to '删除此会员', admin_user_path(@user), :method => :delete, :data => {:confirm => t('delete_confirm')}, :class => 'super danger button' diff --git a/app/views/users/show.mobile.haml b/app/views/users/show.mobile.haml new file mode 100644 index 0000000..11141f9 --- /dev/null +++ b/app/views/users/show.mobile.haml @@ -0,0 +1,35 @@ +- content_for :nav_right do + 第 + = @user.id + 号会员,于 + = @user.created_at.strftime("%Y-%m-%d") + 加入 + +.cell + %table{:cellpadding => 0, :cellspacing => 0, :border => 0, :width => '100%'} + %tr + %td.avatar{:valign => :top} + = medium_avatar(@user) + %td{:style => 'padding-left: 8px;', :valign => :top} + %h1= @user.nickname + .sep10 + - if @personal_website.present? + 个人网站 + = link_to @personal_website, @personal_website, :target => '_blank' + .sep10 + - if @twitter_id.present? + Twitter + = "@#{@twitter_id}" + +- if user_signed_in? and current_user != @user + .cell + - if current_user.following?(@user) + .fr + = link_to '取消特别关注', unfollow_user_path(@user.nickname), :method => :post + %strong.fade + 已关注 + - else + = link_to '加入特别关注', follow_user_path(@user.nickname), :method => :post + += render :partial => 'topics/profile_topic', :collection => @user.latest_created_topics, :as => :topic + diff --git a/app/views/users/topics.html.haml b/app/views/users/topics.html.haml new file mode 100644 index 0000000..6359054 --- /dev/null +++ b/app/views/users/topics.html.haml @@ -0,0 +1,9 @@ +.box + .cell + = build_navigation([@title]) + .cell + = render :partial => 'topics/table_view', :locals => { :topics => @topics } + .inner + .center + = paginate @topics if @topics.any? + diff --git a/app/views/welcome/_home_planes.html.haml b/app/views/welcome/_home_planes.html.haml new file mode 100644 index 0000000..422ccf0 --- /dev/null +++ b/app/views/welcome/_home_planes.html.haml @@ -0,0 +1,6 @@ +.box.fix_cell#planes + .cell + %span.fade + %Strong= Siteconf.site_name + \/ 节点导航 + = render Plane.cached_all(Plane.default_order_str) diff --git a/app/views/welcome/_home_topics.html.haml b/app/views/welcome/_home_topics.html.haml new file mode 100644 index 0000000..ee5d2f8 --- /dev/null +++ b/app/views/welcome/_home_topics.html.haml @@ -0,0 +1,23 @@ +.box#topics_index + .cell{:align => 'left'} + .fr + %span.fade{:style => 'font-size: 110%'} + = Siteconf.marketing.join('  •  ').html_safe + %span.bigger= Siteconf.welcome_tip.html_safe + .sep10 + = Siteconf.splash.html_safe + - if @sticky_topics.any? + - if Siteconf.sticky_topics_heading.present? + #sticky_topics.cell.topics_heading= Siteconf.sticky_topics_heading + = render @sticky_topics + + - if @sticky_topics.any? and Siteconf.latest_topics_heading.present? + #latest_topics.cell.topics_heading= Siteconf.latest_topics_heading + + = render @topics + .inner + .fr= render 'shared/rss' +   + - if Topic.cached_count > Siteconf::HOMEPAGE_TOPICS + %span.chevron » + = link_to '更多新主题', topics_path + '?page=2' diff --git a/app/views/welcome/_rightbar_plane.html.haml b/app/views/welcome/_rightbar_plane.html.haml new file mode 100644 index 0000000..4935341 --- /dev/null +++ b/app/views/welcome/_rightbar_plane.html.haml @@ -0,0 +1,4 @@ +.cell + %span.fade.label= plane.name + .sep5 + = render plane.cached_assoc_collection(:nodes, Node.default_order_str, 20) diff --git a/app/views/welcome/_rightbar_planes.html.haml b/app/views/welcome/_rightbar_planes.html.haml new file mode 100644 index 0000000..04b52b4 --- /dev/null +++ b/app/views/welcome/_rightbar_planes.html.haml @@ -0,0 +1,6 @@ +.box.fix_cell + .cell + 节点导航 + = render :partial => 'rightbar_plane', :collection => Plane.cached_all(Plane.default_order_str), :as => :plane + +.sep20 diff --git a/app/views/welcome/exception.html.haml b/app/views/welcome/exception.html.haml new file mode 100644 index 0000000..6278a71 --- /dev/null +++ b/app/views/welcome/exception.html.haml @@ -0,0 +1,15 @@ +.box + .cell + = build_navigation([@title], 'bigger') + .cell{:align => :center} + %h1.fade= @note + - unless Rails.env.production? + %p{:align => :left} + = @exception.message + = @exception.backtrace.join('
').html_safe + .inner + %span.fade + %span.chevron  ‹  + 返回 + = link_to '首页', root_path + .sep5 diff --git a/app/views/welcome/exception.mobile.haml b/app/views/welcome/exception.mobile.haml new file mode 100644 index 0000000..70ad728 --- /dev/null +++ b/app/views/welcome/exception.mobile.haml @@ -0,0 +1,7 @@ +- add_breadcrumb('页面不存在') +.cell + %h2.fade= @title + - unless Rails.env.production? + %p{:align => :left} + = @exception.message + = @exception.backtrace.join('
').html_safe diff --git a/app/views/welcome/goodbye.html.haml b/app/views/welcome/goodbye.html.haml new file mode 100644 index 0000000..cba5040 --- /dev/null +++ b/app/views/welcome/goodbye.html.haml @@ -0,0 +1,10 @@ +.box + .cell + = build_navigation([@title]) + .inner + = "你已经成功从 #{Siteconf.site_name} 登出。没有任何个人信息留在这台设备上。" + .sep10 + .sep5 + = link_to '重新登入', new_user_session_path, :class => 'super normal button' + .sep5 + diff --git a/app/views/welcome/goodbye.mobile.haml b/app/views/welcome/goodbye.mobile.haml new file mode 100644 index 0000000..b785a04 --- /dev/null +++ b/app/views/welcome/goodbye.mobile.haml @@ -0,0 +1,4 @@ +.cell + = "你已经成功从 #{Siteconf.site_name} 登出。没有任何个人信息留在这台设备上。" + .sep10 + = link_to '重新登入', new_user_session_path diff --git a/app/views/welcome/index.html.haml b/app/views/welcome/index.html.haml new file mode 100644 index 0000000..07f39cc --- /dev/null +++ b/app/views/welcome/index.html.haml @@ -0,0 +1,19 @@ +- if Siteconf.nav_position_sidebar? + - content_for :rightbar do + = render 'rightbar_planes' + +- if Siteconf.show_community_stats? + - content_for :rightbar do + = render 'shared/community_stats' + +- if Siteconf.nav_position_top? + = render 'home_planes' + .sep20 + = render 'home_topics' +- elsif Siteconf.nav_position_bottom? + = render 'home_topics' + .sep20 + = render 'home_planes' +- elsif Siteconf.nav_position_sidebar? + = render 'home_topics' + diff --git a/app/views/welcome/index.mobile.haml b/app/views/welcome/index.mobile.haml new file mode 100644 index 0000000..f755ce0 --- /dev/null +++ b/app/views/welcome/index.mobile.haml @@ -0,0 +1,12 @@ +.section 置顶话题 +#sticky_topics= render @sticky_topics +.section 最新讨论 +#topics_index= render @topics + +- if current_user + .section 我的收藏 + %table{:cellpadding => 10, :cellspacing => 0, :border => 0, :width => '100%'} + = render :partial => 'shared/my_fav', :formats => :html + +.section 节点导航 += render @planes diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..e1d9784 --- /dev/null +++ b/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Rabel::Application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..b515014 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,64 @@ +require File.expand_path('../boot', __FILE__) + +# Pick the frameworks you want: +require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_resource/railtie" +require "sprockets/railtie" +# require "rails/test_unit/railtie" + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(:assets => %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) +end + +module Rabel + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Custom directories with classes and modules you want to be autoloadable. + config.autoload_paths += %W(#{config.root}/lib) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + config.time_zone = 'Beijing' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.default_locale = :zh + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password, :password_confirmation] + + # Enable the asset pipeline + config.assets.enabled = true + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + config.assets.paths += %W(#{config.root}/themes/images #{config.root}/themes/stylesheets #{config.root}/themes/javascripts) + + # enable whitelist mass assignment protection by default + config.active_record.whitelist_attributes = true + + config.before_configuration do + APP_CONFIG = YAML.load_file(Rails.root.join('config', 'settings.yml'))[Rails.env] + config.cache_store = :dalli_store, *APP_CONFIG['memcached']['servers'], {:namespace => APP_CONFIG['memcached']['namespace']} + end + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..4489e58 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000..19b288d --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/config/database.yml.mysql b/config/database.yml.mysql new file mode 100644 index 0000000..3c55339 --- /dev/null +++ b/config/database.yml.mysql @@ -0,0 +1,33 @@ +# MySQL. Versions 4.1 and 5.0 are recommended. +# +# Install the MYSQL driver +# gem install mysql2 +# +# Ensure the MySQL gem is defined in your Gemfile +# gem 'mysql2' +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +default: &default + adapter: mysql2 + encoding: utf8 + reconnect: false + pool: 5 + username: root + password: + socket: /tmp/mysql.sock + +development: + <<: *default + database: rabel_development + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: rabel_test + +production: + <<: *default + database: rabel_production diff --git a/config/database.yml.pg b/config/database.yml.pg new file mode 100644 index 0000000..65de88e --- /dev/null +++ b/config/database.yml.pg @@ -0,0 +1,47 @@ +# PostgreSQL. Versions 7.4 and 8.x are supported. +# +# Install the pg driver: +# gem install pg +# On Mac OS X with macports: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + pool: 5 + username: postgres + password: + host: localhost + port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # The server defaults to notice. + #min_messages: warning + +development: + <<: *default + database: rabel_development + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: rabel_test + +production: + <<: *default + database: rabel_production + diff --git a/config/deploy/example.conf b/config/deploy/example.conf new file mode 100644 index 0000000..4bad500 --- /dev/null +++ b/config/deploy/example.conf @@ -0,0 +1,49 @@ +upstream example_thin { + server 127.0.0.1:3000; +} + +server { + # un-comment below to enable IPv6 support + # listen [::]:80 default ipv6only=on; + listen 80; + + server_name www.example.com example.com; + + # un-comment below to enable host rewrite + # if ($host = 'example.com') { + # rewrite ^(.*)$ http://www.example.com$1 permanent; + # } + + root /home/example/sites/example/public; + index index.html index.htm; + + location ~ ^/(assets|uploads|avatar|favicon) { + access_log off; + expires 1y; + add_header Cache-Control public; + break; + } + + location / { + try_files $uri @example_backend; + } + + location @example_backend { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + client_max_body_size 4M; + client_body_buffer_size 128K; + proxy_pass http://example_thin; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /500.html; + #location = /500.html { + # root /home/ubuntu/rabel/public; + #} +} diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..41af2c0 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +Rabel::Application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..74ec71c --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,39 @@ +Rabel::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + config.action_mailer.raise_delivery_errors = false + config.action_mailer.default_url_options = { :host => 'localhost:3000' } + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = true + config.assets.logger = false + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + config.active_record.auto_explain_threshold_in_seconds = 0.5 +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..fce39b3 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,61 @@ +Rabel::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = true + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = false + + # Generate digests for assets URLs + config.assets.digest = true + + # Defaults to Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Use a different logger for distributed setups + # config.logger = SyslogLogger.new + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + config.assets.precompile += [ Proc.new {|path| File.basename(path).start_with?('i_')} ] + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :sendmail + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..23e69a9 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,43 @@ +Rabel::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + config.cache_store = :null_store +end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..59385cd --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb new file mode 100644 index 0000000..9bb62f6 --- /dev/null +++ b/config/initializers/carrierwave.rb @@ -0,0 +1 @@ +CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..f400a72 --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,207 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. The first +# four configuration values can also be set straight in your models. +Devise.setup do |config| + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class with default "from" parameter. + config.mailer_sender = Settings.system_email + + # Configure the class responsible to send e-mails. + # config.mailer = "Devise::Mailer" + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + config.authentication_keys = [ :nickname ] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [ :nickname ] + + # Tell if authentication through request.params is enabled. True by default. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Basic Auth is enabled. False by default. + # config.http_authenticatable = false + + # If http headers should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. "Application" by default. + # config.http_authentication_realm = "Application" + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 10. If + # using other encryptors, it sets how many times you want the password re-encrypted. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. + config.stretches = Rails.env.test? ? 1 : 10 + + # Setup a pepper to generate the encrypted password. + # config.pepper = "3870d8a048ba70be1b1ec9e6f1bac1e6bd93a96067647e7faa5fb1cf78a7309d3e5d5b8153ce6fc0ce3f3025fab48c25505f8b9f86fc99c43bc4fa77fdb88cc0" + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming his account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming his account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming his account. + # config.allow_unconfirmed_access_for = 2.days + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [ :email ] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + config.remember_for = 1.year + + # If true, a valid remember token can be re-used between multiple browsers. + # config.remember_across_browsers = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # :secure => true in order to force SSL only cookies. + # config.cookie_options = {} + + # ==> Configuration for :validatable + # Range for password length. Default is 6..128. + # config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # an one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + # config.email_regexp = /\A[^@]+@[^@]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [ :email ] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + config.reset_password_keys = [ :nickname, :email ] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 12.hours + + # ==> Configuration for :encryptable + # Allow you to use another encryption algorithm besides bcrypt (default). You can use + # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, + # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) + # and :restful_authentication_sha1 (then you should set stretches to 10, and copy + # REST_AUTH_SITE_KEY to pepper) + # config.encryptor = :sha512 + + # ==> Configuration for :token_authenticatable + # Defines name of the authentication token params key + # config.token_authentication_key = :auth_token + + # If true, authentication through token does not store user in session and needs + # to be supplied on each request. Useful if you are using the token as API token. + # config.stateless_token = false + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Configure sign_out behavior. + # Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope). + # The default is true, which means any logout action will sign out all active scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The :"*/*" and "*/*" formats below is required to match Internet + # Explorer requests. + config.navigational_formats = [:"*/*", "*/*", :html, :mobile] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.failure_app = AnotherApp + # manager.intercept_401 = false + # manager.default_strategies(:scope => :user).unshift :some_external_strategy + # end +end + diff --git a/config/initializers/form_error.rb b/config/initializers/form_error.rb new file mode 100644 index 0000000..af17b04 --- /dev/null +++ b/config/initializers/form_error.rb @@ -0,0 +1,11 @@ +ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| + if html_tag =~ /